From 64a483d11215fa5845969fac5b258ef11bccc2f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 6 Oct 2025 08:29:12 +0200 Subject: [PATCH 001/578] Chore: Adds workflow that triggers plugin schema types update (#111593) --- .github/CODEOWNERS | 1 + .github/workflows/update-schema-types.yml | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 .github/workflows/update-schema-types.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 72635653fe4..bf54aeed6b4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1281,6 +1281,7 @@ embed.go @grafana/grafana-as-code /.github/license_finder.yaml @bergquist /.github/actionlint.yaml @grafana/grafana-developer-enablement-squad /.github/workflows/pr-test-docker.yml @grafana/grafana-developer-enablement-squad +/.github/workflows/update-schema-types.yml @grafana/plugins-platform-frontend # Generated files not requiring owner approval /packages/grafana-data/src/types/featureToggles.gen.ts @grafanabot diff --git a/.github/workflows/update-schema-types.yml b/.github/workflows/update-schema-types.yml new file mode 100644 index 00000000000..a079f1d313e --- /dev/null +++ b/.github/workflows/update-schema-types.yml @@ -0,0 +1,22 @@ +name: Update Schema Types + +on: + push: + branches: + - main + paths: + - docs/sources/developers/plugins/plugin.schema.json + workflow_dispatch: + +# These permissions are needed to assume roles from Github's OIDC. +permissions: + contents: read + id-token: write + +jobs: + bundle-schema-types: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: grafana/plugin-actions/bundle-schema-types@main From 843956e795fcbb6c39f3f03fad973540482dcb8d Mon Sep 17 00:00:00 2001 From: Tania <10127682+undef1nd@users.noreply.github.com> Date: Mon, 6 Oct 2025 09:16:29 +0200 Subject: [PATCH 002/578] OpenFeature: Add tracing to feature flags service (#111895) * Add tracing to feature flags service * Apply review feedback * Apply suggestion from @hairyhenderson Co-authored-by: Dave Henderson * Apply suggestion from @hairyhenderson Co-authored-by: Dave Henderson * Apply suggestion from @hairyhenderson Co-authored-by: Dave Henderson * Apply suggestion from @hairyhenderson Co-authored-by: Dave Henderson * Apply suggestion from @hairyhenderson Co-authored-by: Dave Henderson * Apply suggestion from @hairyhenderson Co-authored-by: Dave Henderson * Fix issues --------- Co-authored-by: Dave Henderson --- pkg/registry/apis/ofrep/proxy.go | 18 +++++++++++++++-- pkg/registry/apis/ofrep/register.go | 31 +++++++++++++++++++++++++---- pkg/registry/apis/ofrep/static.go | 24 ++++++++++++++++++---- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/pkg/registry/apis/ofrep/proxy.go b/pkg/registry/apis/ofrep/proxy.go index efb5cc51930..b5791925b26 100644 --- a/pkg/registry/apis/ofrep/proxy.go +++ b/pkg/registry/apis/ofrep/proxy.go @@ -2,6 +2,7 @@ package ofrep import ( "bytes" + "context" "crypto/tls" "crypto/x509" "encoding/json" @@ -14,14 +15,21 @@ import ( "strconv" "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/util/proxyutil" goffmodel "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/model" ) -func (b *APIBuilder) proxyAllFlagReq(isAuthedUser bool, w http.ResponseWriter, r *http.Request) { +func (b *APIBuilder) proxyAllFlagReq(ctx context.Context, isAuthedUser bool, w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(ctx, "ofrep.proxy.evalAllFlags") + defer span.End() + + r = r.WithContext(ctx) + proxy, err := b.newProxy(ofrepPath) if err != nil { + err = tracing.Error(span, err) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -61,9 +69,15 @@ func (b *APIBuilder) proxyAllFlagReq(isAuthedUser bool, w http.ResponseWriter, r proxy.ServeHTTP(w, r) } -func (b *APIBuilder) proxyFlagReq(flagKey string, isAuthedUser bool, w http.ResponseWriter, r *http.Request) { +func (b *APIBuilder) proxyFlagReq(ctx context.Context, flagKey string, isAuthedUser bool, w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(ctx, "ofrep.proxy.evalFlag") + defer span.End() + + r = r.WithContext(ctx) + proxy, err := b.newProxy(path.Join(ofrepPath, flagKey)) if err != nil { + err = tracing.Error(span, err) b.logger.Error("Failed to create proxy", "key", flagKey, "error", err) http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/pkg/registry/apis/ofrep/register.go b/pkg/registry/apis/ofrep/register.go index 154e3f6b6fc..b765bbadc32 100644 --- a/pkg/registry/apis/ofrep/register.go +++ b/pkg/registry/apis/ofrep/register.go @@ -10,6 +10,9 @@ import ( "net/url" "github.com/gorilla/mux" + "github.com/grafana/grafana/pkg/infra/tracing" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -31,6 +34,8 @@ var _ builder.APIGroupBuilder = (*APIBuilder)(nil) var _ builder.APIGroupRouteProvider = (*APIBuilder)(nil) var _ builder.APIGroupVersionProvider = (*APIBuilder)(nil) +var tracer = otel.Tracer("github.com/grafana/grafana/pkg/registry/apis/ofrep") + const ofrepPath = "/ofrep/v1/evaluate/flags" const namespaceMismatchMsg = "rejecting request with namespace mismatch" @@ -240,7 +245,13 @@ func (b *APIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIRoutes { } func (b *APIBuilder) oneFlagHandler(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "ofrep.handler.evalFlag") + defer span.End() + + r = r.WithContext(ctx) + if !b.validateNamespace(r) { + _ = tracing.Errorf(span, namespaceMismatchMsg) b.logger.Error(namespaceMismatchMsg) http.Error(w, namespaceMismatchMsg, http.StatusUnauthorized) return @@ -248,42 +259,54 @@ func (b *APIBuilder) oneFlagHandler(w http.ResponseWriter, r *http.Request) { flagKey := mux.Vars(r)["flagKey"] if flagKey == "" { + _ = tracing.Errorf(span, "flagKey parameter is required") http.Error(w, "flagKey parameter is required", http.StatusBadRequest) return } + span.SetAttributes(attribute.String("flag_key", flagKey)) + isAuthedReq := b.isAuthenticatedRequest(r) + span.SetAttributes(attribute.Bool("authenticated", isAuthedReq)) // Unless the request is authenticated, we only allow public flags evaluations if !isAuthedReq && !isPublicFlag(flagKey) { + _ = tracing.Errorf(span, "unauthorized to evaluate flag: %s", flagKey) b.logger.Error("Unauthorized to evaluate flag", "flagKey", flagKey) http.Error(w, "unauthorized to evaluate flag", http.StatusUnauthorized) return } if b.providerType == setting.GOFFProviderType { - b.proxyFlagReq(flagKey, isAuthedReq, w, r) + b.proxyFlagReq(ctx, flagKey, isAuthedReq, w, r) return } - b.evalFlagStatic(flagKey, w, r) + b.evalFlagStatic(ctx, flagKey, w) } func (b *APIBuilder) allFlagsHandler(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "ofrep.handler.evalAllFlags") + defer span.End() + + r = r.WithContext(ctx) + if !b.validateNamespace(r) { + _ = tracing.Errorf(span, namespaceMismatchMsg) b.logger.Error(namespaceMismatchMsg) http.Error(w, namespaceMismatchMsg, http.StatusUnauthorized) return } isAuthedReq := b.isAuthenticatedRequest(r) + span.SetAttributes(attribute.Bool("authenticated", isAuthedReq)) if b.providerType == setting.GOFFProviderType { - b.proxyAllFlagReq(isAuthedReq, w, r) + b.proxyAllFlagReq(ctx, isAuthedReq, w, r) return } - b.evalAllFlagsStatic(isAuthedReq, w, r) + b.evalAllFlagsStatic(ctx, isAuthedReq, w) } func writeResponse(statusCode int, result any, logger log.Logger, w http.ResponseWriter) { diff --git a/pkg/registry/apis/ofrep/static.go b/pkg/registry/apis/ofrep/static.go index 8d56104649d..18ff794349d 100644 --- a/pkg/registry/apis/ofrep/static.go +++ b/pkg/registry/apis/ofrep/static.go @@ -1,19 +1,28 @@ package ofrep import ( + "context" "net/http" + "github.com/grafana/grafana/pkg/infra/tracing" goffmodel "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/model" + "go.opentelemetry.io/otel/attribute" ) -func (b *APIBuilder) evalAllFlagsStatic(isAuthedUser bool, w http.ResponseWriter, r *http.Request) { - result, err := b.staticEvaluator.EvalAllFlags(r.Context()) +func (b *APIBuilder) evalAllFlagsStatic(ctx context.Context, isAuthedUser bool, w http.ResponseWriter) { + _, span := tracer.Start(ctx, "ofrep.static.evalAllFlags") + defer span.End() + + result, err := b.staticEvaluator.EvalAllFlags(ctx) if err != nil { + err = tracing.Error(span, err) b.logger.Error("Failed to evaluate all static flags", "error", err) http.Error(w, "failed to evaluate flags", http.StatusInternalServerError) return } + span.SetAttributes(attribute.Int("total_flags_count", len(result.Flags))) + if !isAuthedUser { var publicOnly []goffmodel.OFREPFlagBulkEvaluateSuccessResponse @@ -24,14 +33,21 @@ func (b *APIBuilder) evalAllFlagsStatic(isAuthedUser bool, w http.ResponseWriter } result.Flags = publicOnly + span.SetAttributes(attribute.Int("public_flags_count", len(publicOnly))) } writeResponse(http.StatusOK, result, b.logger, w) } -func (b *APIBuilder) evalFlagStatic(flagKey string, w http.ResponseWriter, r *http.Request) { - result, err := b.staticEvaluator.EvalFlag(r.Context(), flagKey) +func (b *APIBuilder) evalFlagStatic(ctx context.Context, flagKey string, w http.ResponseWriter) { + _, span := tracer.Start(ctx, "ofrep.static.evalFlag") + defer span.End() + + span.SetAttributes(attribute.String("flag_key", flagKey)) + + result, err := b.staticEvaluator.EvalFlag(ctx, flagKey) if err != nil { + err = tracing.Error(span, err) b.logger.Error("Failed to evaluate static flag", "key", flagKey, "error", err) http.Error(w, "failed to evaluate flag", http.StatusInternalServerError) return From cd889fef9b4bb87c934be66de58c4b84cecd09f8 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Mon, 6 Oct 2025 09:28:38 +0200 Subject: [PATCH 003/578] Alerting: Keep extra configurations on main config update (#106958) --- .../ngalert/notifier/alertmanager_config.go | 24 ++ .../notifier/alertmanager_config_test.go | 258 ++++++++++++++++++ 2 files changed, 282 insertions(+) diff --git a/pkg/services/ngalert/notifier/alertmanager_config.go b/pkg/services/ngalert/notifier/alertmanager_config.go index 9f101621ef8..514da9be686 100644 --- a/pkg/services/ngalert/notifier/alertmanager_config.go +++ b/pkg/services/ngalert/notifier/alertmanager_config.go @@ -324,6 +324,15 @@ func (moa *MultiOrgAlertmanager) SaveAndApplyAlertmanagerConfiguration(ctx conte } cleanPermissionsErr := err + if previousConfig != nil { + // If there is a previous configuration, we need to copy its extra configs to the new one. + extraConfigs, err := extractExtraConfigs(previousConfig.AlertmanagerConfiguration) + if err != nil { + return fmt.Errorf("failed to extract extra configs from previous configuration: %w", err) + } + config.ExtraConfigs = extraConfigs + } + if err := moa.Crypto.ProcessSecureSettings(ctx, org, config.AlertmanagerConfig.Receivers); err != nil { return fmt.Errorf("failed to post process Alertmanager configuration: %w", err) } @@ -572,3 +581,18 @@ func extractReceiverNames(rawConfig string) (sets.Set[string], error) { return receiverNames, nil } + +// extractExtraConfigs extracts encrypted (does not decrypt) extra configurations from the raw Alertmanager config. +func extractExtraConfigs(rawConfig string) ([]definitions.ExtraConfiguration, error) { + // Slimmed down version of the Alertmanager configuration to extract extra configs. + type extraConfigUserConfig struct { + ExtraConfigs []definitions.ExtraConfiguration `yaml:"extra_config,omitempty" json:"extra_config,omitempty"` + } + + cfg := &extraConfigUserConfig{} + if err := json.Unmarshal([]byte(rawConfig), cfg); err != nil { + return nil, fmt.Errorf("unable to parse Alertmanager configuration: %w", err) + } + + return cfg.ExtraConfigs, nil +} diff --git a/pkg/services/ngalert/notifier/alertmanager_config_test.go b/pkg/services/ngalert/notifier/alertmanager_config_test.go index 3b12153da98..f4b06285db5 100644 --- a/pkg/services/ngalert/notifier/alertmanager_config_test.go +++ b/pkg/services/ngalert/notifier/alertmanager_config_test.go @@ -150,6 +150,264 @@ receivers: }) } +func TestMultiOrgAlertmanager_SaveAndApplyAlertmanagerConfiguration(t *testing.T) { + orgID := int64(1) + ctx := context.Background() + + t.Run("SaveAndApplyAlertmanagerConfiguration preserves existing extra configs", func(t *testing.T) { + mam := setupMam(t, nil) + require.NoError(t, mam.LoadAndSyncAlertmanagersForOrgs(ctx)) + + extraConfig := definitions.ExtraConfiguration{ + Identifier: "test-extra-config", + MergeMatchers: amconfig.Matchers{&labels.Matcher{Type: labels.MatchEqual, Name: "env", Value: "test"}}, + TemplateFiles: map[string]string{"test.tmpl": "{{ define \"test\" }}Test{{ end }}"}, + AlertmanagerConfig: `route: + receiver: extra-receiver +receivers: + - name: extra-receiver`, + } + + err := mam.SaveAndApplyExtraConfiguration(ctx, orgID, extraConfig) + require.NoError(t, err) + + // Verify extra config was saved + gettableConfig, err := mam.GetAlertmanagerConfiguration(ctx, orgID, false, false) + require.NoError(t, err) + require.Len(t, gettableConfig.ExtraConfigs, 1) + require.Equal(t, extraConfig.Identifier, gettableConfig.ExtraConfigs[0].Identifier) + + // Apply a new main configuration + newMainConfig := definitions.PostableUserConfig{ + AlertmanagerConfig: definitions.PostableApiAlertingConfig{ + Config: definitions.Config{ + Route: &definitions.Route{ + Receiver: "main-receiver", + }, + }, + Receivers: []*definitions.PostableApiReceiver{ + { + Receiver: amconfig.Receiver{ + Name: "main-receiver", + }, + PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{ + GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{ + { + Name: "main-receiver", + Type: "email", + Settings: definitions.RawMessage(`{"addresses": "me@grafana.com"}`), + }, + }, + }, + }, + }, + }, + } + + err = mam.SaveAndApplyAlertmanagerConfiguration(ctx, orgID, newMainConfig) + require.NoError(t, err) + + // Verify that the extra config is still present after applying the new main config + updatedConfig, err := mam.GetAlertmanagerConfiguration(ctx, orgID, false, false) + require.NoError(t, err) + require.Len(t, updatedConfig.ExtraConfigs, 1) + require.Equal(t, extraConfig.Identifier, updatedConfig.ExtraConfigs[0].Identifier) + require.Equal(t, extraConfig.TemplateFiles, updatedConfig.ExtraConfigs[0].TemplateFiles) + + // Verify the main config was updated + require.Equal(t, "main-receiver", updatedConfig.AlertmanagerConfig.Route.Receiver) + require.Len(t, updatedConfig.AlertmanagerConfig.Receivers, 1) + require.Equal(t, "main-receiver", updatedConfig.AlertmanagerConfig.Receivers[0].Name) + }) + + t.Run("SaveAndApplyAlertmanagerConfiguration handles missing extra_configs field", func(t *testing.T) { + mam := setupMam(t, nil) + require.NoError(t, mam.LoadAndSyncAlertmanagersForOrgs(ctx)) + + // Apply initial config without extra_configs field + initialConfig := definitions.PostableUserConfig{ + AlertmanagerConfig: definitions.PostableApiAlertingConfig{ + Config: definitions.Config{ + Route: &definitions.Route{ + Receiver: "initial-receiver", + }, + }, + Receivers: []*definitions.PostableApiReceiver{ + { + Receiver: amconfig.Receiver{ + Name: "initial-receiver", + }, + PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{ + GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{ + { + Name: "initial-receiver", + Type: "email", + Settings: definitions.RawMessage(`{"addresses": "initial@grafana.com"}`), + }, + }, + }, + }, + }, + }, + } + + err := mam.SaveAndApplyAlertmanagerConfiguration(ctx, orgID, initialConfig) + require.NoError(t, err) + + // Apply a new main configuration + newMainConfig := definitions.PostableUserConfig{ + AlertmanagerConfig: definitions.PostableApiAlertingConfig{ + Config: definitions.Config{ + Route: &definitions.Route{ + Receiver: "main-receiver", + }, + }, + Receivers: []*definitions.PostableApiReceiver{ + { + Receiver: amconfig.Receiver{ + Name: "main-receiver", + }, + PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{ + GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{ + { + Name: "main-receiver", + Type: "email", + Settings: definitions.RawMessage(`{"addresses": "me@grafana.com"}`), + }, + }, + }, + }, + }, + }, + } + + err = mam.SaveAndApplyAlertmanagerConfiguration(ctx, orgID, newMainConfig) + require.NoError(t, err) + + // Verify that no extra configs are present and main config was updated + updatedConfig, err := mam.GetAlertmanagerConfiguration(ctx, orgID, false, false) + require.NoError(t, err) + require.Len(t, updatedConfig.ExtraConfigs, 0) + require.Equal(t, "main-receiver", updatedConfig.AlertmanagerConfig.Route.Receiver) + }) + + t.Run("SaveAndApplyAlertmanagerConfiguration handles empty extra_configs array", func(t *testing.T) { + mam := setupMam(t, nil) + require.NoError(t, mam.LoadAndSyncAlertmanagersForOrgs(ctx)) + + // Apply initial config with empty extra_configs + initialConfig := definitions.PostableUserConfig{ + AlertmanagerConfig: definitions.PostableApiAlertingConfig{ + Config: definitions.Config{ + Route: &definitions.Route{ + Receiver: "initial-receiver", + }, + }, + Receivers: []*definitions.PostableApiReceiver{ + { + Receiver: amconfig.Receiver{ + Name: "initial-receiver", + }, + PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{ + GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{ + { + Name: "initial-receiver", + Type: "email", + Settings: definitions.RawMessage(`{"addresses": "initial@grafana.com"}`), + }, + }, + }, + }, + }, + }, + ExtraConfigs: []definitions.ExtraConfiguration{}, // Empty array + } + + err := mam.SaveAndApplyAlertmanagerConfiguration(ctx, orgID, initialConfig) + require.NoError(t, err) + + // Apply a new main configuration + newMainConfig := definitions.PostableUserConfig{ + AlertmanagerConfig: definitions.PostableApiAlertingConfig{ + Config: definitions.Config{ + Route: &definitions.Route{ + Receiver: "main-receiver", + }, + }, + Receivers: []*definitions.PostableApiReceiver{ + { + Receiver: amconfig.Receiver{ + Name: "main-receiver", + }, + PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{ + GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{ + { + Name: "main-receiver", + Type: "email", + Settings: definitions.RawMessage(`{"addresses": "me@grafana.com"}`), + }, + }, + }, + }, + }, + }, + } + + err = mam.SaveAndApplyAlertmanagerConfiguration(ctx, orgID, newMainConfig) + require.NoError(t, err) + + // Verify that no extra configs are present and main config was updated + updatedConfig, err := mam.GetAlertmanagerConfiguration(ctx, orgID, false, false) + require.NoError(t, err) + require.Len(t, updatedConfig.ExtraConfigs, 0) + require.Equal(t, "main-receiver", updatedConfig.AlertmanagerConfig.Route.Receiver) + }) +} + +func TestExtractExtraConfigs(t *testing.T) { + t.Run("extracts extra configs from JSON", func(t *testing.T) { + jsonConfig := `{ + "extra_config": [ + { + "identifier": "test-config", + "merge_matchers": [], + "template_files": {"test.tmpl": "test"}, + "alertmanager_config": "route:\n receiver: test" + } + ] + }` + + extraConfigs, err := extractExtraConfigs(jsonConfig) + require.NoError(t, err) + require.Len(t, extraConfigs, 1) + require.Equal(t, "test-config", extraConfigs[0].Identifier) + }) + + t.Run("handles missing extra_config field", func(t *testing.T) { + jsonConfig := `{"alertmanager_config": {"route": {"receiver": "test"}}}` + + extraConfigs, err := extractExtraConfigs(jsonConfig) + require.NoError(t, err) + require.Len(t, extraConfigs, 0) + }) + + t.Run("handles empty extra_config array", func(t *testing.T) { + jsonConfig := `{"extra_config": []}` + + extraConfigs, err := extractExtraConfigs(jsonConfig) + require.NoError(t, err) + require.Len(t, extraConfigs, 0) + }) + + t.Run("handles null extra_config", func(t *testing.T) { + jsonConfig := `{"extra_config": null}` + + extraConfigs, err := extractExtraConfigs(jsonConfig) + require.NoError(t, err) + require.Len(t, extraConfigs, 0) + }) +} + func TestMultiOrgAlertmanager_DeleteExtraConfiguration(t *testing.T) { orgID := int64(1) From a44af810822ea901d223c11258499e6adab4424f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Mon, 6 Oct 2025 10:02:03 +0200 Subject: [PATCH 004/578] Unified storage search: Introduce min index update interval (#111978) * Don't update index more often than specified index_min_update_interval. * Add artificial sleep at the end of write operations. * Improve test: check for number of update calls, make diff check less flaky. * Make test less flaky by allowing for higher diff variance. * Make test less flaky by allowing for expected update calls variance. --- pkg/setting/setting.go | 1 + pkg/setting/setting_unified_storage.go | 1 + pkg/storage/unified/resource/server.go | 49 +++++++++ pkg/storage/unified/resource/server_test.go | 29 ++++++ pkg/storage/unified/search/bleve.go | 57 ++++++++--- pkg/storage/unified/search/bleve_test.go | 106 +++++++++++++++++++- pkg/storage/unified/search/options.go | 32 +++--- 7 files changed, 244 insertions(+), 31 deletions(-) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 11071c2a9fc..89a42e9d56d 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -582,6 +582,7 @@ type Cfg struct { IndexMinCount int IndexRebuildInterval time.Duration IndexCacheTTL time.Duration + IndexMinUpdateInterval time.Duration // Don't update index if it was updated less than this interval ago. MaxFileIndexAge time.Duration // Max age of file-based indexes. Index older than this will be rebuilt asynchronously. MinFileIndexBuildVersion string // Minimum version of Grafana that built the file-based index. If index was built with older Grafana, it will be rebuilt asynchronously. EnableSharding bool diff --git a/pkg/setting/setting_unified_storage.go b/pkg/setting/setting_unified_storage.go index 0ebb6c93830..914d5f789d7 100644 --- a/pkg/setting/setting_unified_storage.go +++ b/pkg/setting/setting_unified_storage.go @@ -73,6 +73,7 @@ func (cfg *Cfg) setUnifiedStorageConfig() { // default to 24 hours because usage insights summarizes the data every 24 hours cfg.IndexRebuildInterval = section.Key("index_rebuild_interval").MustDuration(24 * time.Hour) cfg.IndexCacheTTL = section.Key("index_cache_ttl").MustDuration(10 * time.Minute) + cfg.IndexMinUpdateInterval = section.Key("index_min_update_interval").MustDuration(0) cfg.SprinklesApiServer = section.Key("sprinkles_api_server").String() cfg.SprinklesApiServerPageLimit = section.Key("sprinkles_api_server_page_limit").MustInt(10000) cfg.CACertPath = section.Key("ca_cert_path").String() diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 17c36996ad9..bedea3987c2 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -22,6 +22,7 @@ import ( claims "github.com/grafana/authlib/types" "github.com/grafana/dskit/backoff" + "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apimachinery/validation" secrets "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" @@ -194,6 +195,10 @@ type SearchOptions struct { // Number of workers to use for index rebuilds. IndexRebuildWorkers int + + // Minimum time between index updates. This is also used as a delay after a successful write operation, to guarantee + // that subsequent search will observe the effect of the writing. + IndexMinUpdateInterval time.Duration } type ResourceServerOptions struct { @@ -336,6 +341,8 @@ func NewResourceServer(opts ResourceServerOptions) (*server, error) { reg: opts.Reg, queue: opts.QOSQueue, queueConfig: opts.QOSConfig, + + artificialSuccessfulWriteDelay: opts.Search.IndexMinUpdateInterval, } if opts.Search.Resources != nil { @@ -386,6 +393,11 @@ type server struct { reg prometheus.Registerer queue QOSEnqueuer queueConfig QueueConfig + + // This value is used by storage server to artificially delay returning response after successful + // write operations to make sure that subsequent search by the same client will return up-to-date results. + // Set from SearchOptions.IndexMinUpdateInterval. + artificialSuccessfulWriteDelay time.Duration } // Init implements ResourceServer. @@ -661,6 +673,8 @@ func (s *server) Create(ctx context.Context, req *resourcepb.CreateRequest) (*re }) } + s.sleepAfterSuccessfulWriteOperation(res, err) + return res, err } @@ -684,6 +698,37 @@ func (s *server) create(ctx context.Context, user claims.AuthInfo, req *resource return rsp, nil } +type responseWithErrorResult interface { + GetError() *resourcepb.ErrorResult +} + +// sleepAfterSuccessfulWriteOperation will sleep for a specified time if the operation was successful. +// Returns boolean indicating whether the sleep was performed or not (used in testing). +// +// This sleep is performed to guarantee search-after-write consistency, when rate-limiting updates to search index. +func (s *server) sleepAfterSuccessfulWriteOperation(res responseWithErrorResult, err error) bool { + if s.artificialSuccessfulWriteDelay <= 0 { + return false + } + + if err != nil { + // No sleep necessary if operation failed. + return false + } + + // We expect that non-nil interface values with typed nils can still handle GetError() call. + if res != nil { + errRes := res.GetError() + if errRes != nil { + // No sleep necessary if operation failed. + return false + } + } + + time.Sleep(s.artificialSuccessfulWriteDelay) + return true +} + func (s *server) Update(ctx context.Context, req *resourcepb.UpdateRequest) (*resourcepb.UpdateResponse, error) { ctx, span := s.tracer.Start(ctx, "storage_server.Update") defer span.End() @@ -715,6 +760,8 @@ func (s *server) Update(ctx context.Context, req *resourcepb.UpdateRequest) (*re }) } + s.sleepAfterSuccessfulWriteOperation(res, err) + return res, err } @@ -787,6 +834,8 @@ func (s *server) Delete(ctx context.Context, req *resourcepb.DeleteRequest) (*re }) } + s.sleepAfterSuccessfulWriteOperation(res, err) + return res, err } diff --git a/pkg/storage/unified/resource/server_test.go b/pkg/storage/unified/resource/server_test.go index 7e09184d7ad..66536b4d5d8 100644 --- a/pkg/storage/unified/resource/server_test.go +++ b/pkg/storage/unified/resource/server_test.go @@ -3,6 +3,7 @@ package resource import ( "context" "encoding/json" + "errors" "fmt" "log/slog" "net/http" @@ -21,6 +22,7 @@ import ( authlib "github.com/grafana/authlib/types" "github.com/grafana/dskit/services" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/log" @@ -587,3 +589,30 @@ func newTestServerWithQueue(t *testing.T, maxSizePerTenant int, numWorkers int) } return s, q } + +func TestArtificialDelayAfterSuccessfulOperation(t *testing.T) { + s := &server{artificialSuccessfulWriteDelay: 1 * time.Millisecond} + + check := func(t *testing.T, expectedSleep bool, res responseWithErrorResult, err error) { + slept := s.sleepAfterSuccessfulWriteOperation(res, err) + require.Equal(t, expectedSleep, slept) + } + + // Successful responses should sleep + check(t, true, nil, nil) + + check(t, true, (responseWithErrorResult)((*resourcepb.CreateResponse)(nil)), nil) + check(t, true, &resourcepb.CreateResponse{}, nil) + + check(t, true, (responseWithErrorResult)((*resourcepb.UpdateResponse)(nil)), nil) + check(t, true, &resourcepb.UpdateResponse{}, nil) + + check(t, true, (responseWithErrorResult)((*resourcepb.DeleteResponse)(nil)), nil) + check(t, true, &resourcepb.DeleteResponse{}, nil) + + // Failed responses should return without sleeping + check(t, false, nil, errors.New("some error")) + check(t, false, &resourcepb.CreateResponse{Error: AsErrorResult(errors.New("some error"))}, nil) + check(t, false, &resourcepb.UpdateResponse{Error: AsErrorResult(errors.New("some error"))}, nil) + check(t, false, &resourcepb.DeleteResponse{Error: AsErrorResult(errors.New("some error"))}, nil) +} diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 211bef376cc..d8ac41e1332 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -79,6 +79,9 @@ type BleveOptions struct { UseFullNgram bool + // Minimum time between index updates. + IndexMinUpdateInterval time.Duration + // This function is called to check whether the index is owned by the current instance. // Indexes that are not owned by current instance are eligible for cleanup. // If nil, all indexes are owned by the current instance. @@ -689,8 +692,8 @@ func (b *bleveBackend) closeAllIndexes() { } type updateRequest struct { - reason string - callback chan updateResult + requestTime time.Time + callback chan updateResult } type updateResult struct { @@ -705,6 +708,10 @@ type bleveIndex struct { // RV returned by last List/ListModifiedSince operation. Updated when updating index. resourceVersion int64 + // Timestamp when the last update to the index was done (started). + // Subsequent update requests only trigger new update if minUpdateInterval has elapsed. + nextUpdateTime time.Time + standard resource.SearchableDocumentFields fields resource.SearchableDocumentFields @@ -719,7 +726,8 @@ type bleveIndex struct { tracing trace.Tracer logger *slog.Logger - updaterFn resource.UpdateFn + updaterFn resource.UpdateFn + minUpdateInterval time.Duration updaterMu sync.Mutex updaterCond *sync.Cond // Used to signal the updater goroutine that there is work to do, or updater is no longer enabled and should stop. Also used by updater itself to stop early if there's no work to be done. @@ -746,15 +754,16 @@ func (b *bleveBackend) newBleveIndex( logger *slog.Logger, ) *bleveIndex { bi := &bleveIndex{ - key: key, - index: index, - indexStorage: newIndexType, - fields: fields, - allFields: allFields, - standard: standardSearchFields, - tracing: b.tracer, - logger: logger, - updaterFn: updaterFn, + key: key, + index: index, + indexStorage: newIndexType, + fields: fields, + allFields: allFields, + standard: standardSearchFields, + tracing: b.tracer, + logger: logger, + updaterFn: updaterFn, + minUpdateInterval: b.opts.IndexMinUpdateInterval, } bi.updaterCond = sync.NewCond(&bi.updaterMu) if b.indexMetrics != nil { @@ -1356,7 +1365,7 @@ func (b *bleveIndex) UpdateIndex(ctx context.Context, reason string) (int64, err } // Use chan with buffer size 1 to ensure that we can always send the result back, even if there's no reader anymore. - req := updateRequest{reason: reason, callback: make(chan updateResult, 1)} + req := updateRequest{requestTime: time.Now(), callback: make(chan updateResult, 1)} // Make sure that the updater goroutine is running. b.updaterMu.Lock() @@ -1413,7 +1422,7 @@ func (b *bleveIndex) runUpdater(ctx context.Context) { b.updaterMu.Lock() for !b.updaterShutdown && ctx.Err() == nil && len(b.updaterQueue) == 0 && time.Since(start) < maxWait { - // Cond is signalled when updaterShutdown changes, updaterQueue gets new element or when timeout occurs. + // Cond is signaled when updaterShutdown changes, updaterQueue gets new element or when timeout occurs. b.updaterCond.Wait() } @@ -1436,6 +1445,26 @@ func (b *bleveIndex) runUpdater(ctx context.Context) { return } + // Check if requests arrived before minUpdateInterval since the last update has elapsed, and remove such requests. + for ix := 0; ix < len(batch); { + req := batch[ix] + if req.requestTime.Before(b.nextUpdateTime) { + req.callback <- updateResult{rv: b.resourceVersion} + batch = append(batch[:ix], batch[ix+1:]...) + } else { + // Keep in the batch + ix++ + } + } + + // If all requests are now handled, don't perform update. + if len(batch) == 0 { + continue + } + + // Bump next update time + b.nextUpdateTime = time.Now().Add(b.minUpdateInterval) + var rv int64 var err = ctx.Err() if err == nil { diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index d57196b842d..2c767e079df 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -833,6 +833,12 @@ func withOwnsIndexFn(fn func(key resource.NamespacedResource) (bool, error)) set } } +func withIndexMinUpdateInterval(d time.Duration) setupOption { + return func(options *BleveOptions) { + options.IndexMinUpdateInterval = d + } +} + func TestBuildIndexExpiration(t *testing.T) { ns := resource.NamespacedResource{ Namespace: "test", @@ -1132,6 +1138,37 @@ func updateTestDocs(ns resource.NamespacedResource, docs int) resource.UpdateFn } } +func updateTestDocsReturningMillisTimestamp(ns resource.NamespacedResource, docs int) (resource.UpdateFn, *atomic.Int64) { + cnt := 0 + updateCalls := atomic.NewInt64(0) + + return func(context context.Context, index resource.ResourceIndex, sinceRV int64) (newRV int64, updatedDocs int, _ error) { + now := time.Now() + updateCalls.Inc() + + cnt++ + + var items []*resource.BulkIndexItem + for i := 0; i < docs; i++ { + items = append(items, &resource.BulkIndexItem{ + Action: resource.ActionIndex, + Doc: &resource.IndexableDocument{ + Key: &resourcepb.ResourceKey{ + Namespace: ns.Namespace, + Group: ns.Group, + Resource: ns.Resource, + Name: fmt.Sprintf("doc%d", i), + }, + Title: fmt.Sprintf("Document %d (gen_%d)", i, cnt), + }, + }) + } + + err := index.BulkIndex(&resource.BulkIndexRequest{Items: items}) + return now.UnixMilli(), docs, err + }, updateCalls +} + func TestCleanOldIndexes(t *testing.T) { dir := t.TempDir() @@ -1353,7 +1390,7 @@ func TestConcurrentIndexUpdateSearchAndRebuild(t *testing.T) { cancel() wg.Wait() - fmt.Println("Updates:", updates.Load(), "searches:", searches.Load(), "rebuilds:", rebuilds.Load()) + t.Log("Updates:", updates.Load(), "searches:", searches.Load(), "rebuilds:", rebuilds.Load()) } // Verify concurrent updates and searches work as expected. @@ -1415,7 +1452,72 @@ func TestConcurrentIndexUpdateAndSearch(t *testing.T) { require.Greater(t, rvUpdatedByMultipleGoroutines, int64(0)) } -// Verify concurrent updates and searches work as expected. +func TestConcurrentIndexUpdateAndSearchWithIndexMinUpdateInterval(t *testing.T) { + ns := resource.NamespacedResource{ + Namespace: "test", + Group: "group", + Resource: "resource", + } + + const minInterval = 100 * time.Millisecond + be, _ := setupBleveBackend(t, withIndexMinUpdateInterval(minInterval)) + + updateFn, updateCalls := updateTestDocsReturningMillisTimestamp(ns, 5) + idx, err := be.BuildIndex(t.Context(), ns, 10 /* file based */, nil, "test", indexTestDocs(ns, 10, 100), updateFn, false) + require.NoError(t, err) + + wg := sync.WaitGroup{} + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + attemptedUpdates := atomic.NewInt64(0) + + // Verify that each returned RV (unix timestamp in millis) is either the same as before, or at least minInterval later. + const searchConcurrency = 25 + for i := 0; i < searchConcurrency; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + prevRV := int64(0) + for ctx.Err() == nil { + attemptedUpdates.Inc() + + // We use t.Context() here to avoid getting errors from context cancellation. + rv, err := idx.UpdateIndex(t.Context(), "test") + require.NoError(t, err) + + // Our update function returns unix timestamp in millis. We expect it to not change at all, or change by minInterval. + if prevRV > 0 { + rvDiff := rv - prevRV + if rvDiff == 0 { + // OK + } else { + // Allow returned RV to be within 10% of minInterval. + require.InDelta(t, minInterval.Milliseconds(), rvDiff, float64(minInterval.Milliseconds())*0.10) + } + } + + prevRV = rv + require.Equal(t, int64(10), searchTitle(t, idx, "Document", 10, ns).TotalHits) + } + }() + } + + // Run updates and searches for this time. + testTime := 1 * time.Second + + time.Sleep(testTime) + cancel() + wg.Wait() + + expectedUpdateCalls := int64(testTime / minInterval) + require.InDelta(t, expectedUpdateCalls, updateCalls.Load(), float64(expectedUpdateCalls/2)) + require.Greater(t, attemptedUpdates.Load(), updateCalls.Load()) + + t.Log("Attempted updates:", attemptedUpdates.Load(), "update calls:", updateCalls.Load()) +} + func TestIndexUpdateWithErrors(t *testing.T) { ns := resource.NamespacedResource{ Namespace: "test", diff --git a/pkg/storage/unified/search/options.go b/pkg/storage/unified/search/options.go index a719bebf32c..0d856b586c3 100644 --- a/pkg/storage/unified/search/options.go +++ b/pkg/storage/unified/search/options.go @@ -41,13 +41,14 @@ func NewSearchOptions( } bleve, err := NewBleveBackend(BleveOptions{ - Root: root, - FileThreshold: int64(cfg.IndexFileThreshold), // fewer than X items will use a memory index - BatchSize: cfg.IndexMaxBatchSize, // This is the batch size for how many objects to add to the index at once - IndexCacheTTL: cfg.IndexCacheTTL, // How long to keep the index cache in memory - BuildVersion: cfg.BuildVersion, - UseFullNgram: features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageUseFullNgram), - OwnsIndex: ownsIndexFn, + Root: root, + FileThreshold: int64(cfg.IndexFileThreshold), // fewer than X items will use a memory index + BatchSize: cfg.IndexMaxBatchSize, // This is the batch size for how many objects to add to the index at once + IndexCacheTTL: cfg.IndexCacheTTL, // How long to keep the index cache in memory + BuildVersion: cfg.BuildVersion, + UseFullNgram: features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageUseFullNgram), + OwnsIndex: ownsIndexFn, + IndexMinUpdateInterval: cfg.IndexMinUpdateInterval, }, tracer, indexMetrics) if err != nil { @@ -55,14 +56,15 @@ func NewSearchOptions( } return resource.SearchOptions{ - Backend: bleve, - Resources: docs, - InitWorkerThreads: cfg.IndexWorkers, - IndexRebuildWorkers: cfg.IndexRebuildWorkers, - InitMinCount: cfg.IndexMinCount, - DashboardIndexMaxAge: cfg.IndexRebuildInterval, - MaxIndexAge: cfg.MaxFileIndexAge, - MinBuildVersion: minVersion, + Backend: bleve, + Resources: docs, + InitWorkerThreads: cfg.IndexWorkers, + IndexRebuildWorkers: cfg.IndexRebuildWorkers, + InitMinCount: cfg.IndexMinCount, + DashboardIndexMaxAge: cfg.IndexRebuildInterval, + MaxIndexAge: cfg.MaxFileIndexAge, + MinBuildVersion: minVersion, + IndexMinUpdateInterval: cfg.IndexMinUpdateInterval, }, nil } return resource.SearchOptions{}, nil From 601f7cda348fcebc3bac9f9fb0efc0f6cdaa67d5 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Mon, 6 Oct 2025 10:31:30 +0200 Subject: [PATCH 005/578] CloudMigrations: Increase timeout of eventual checks and add debug message in flaky test (#112042) * CloudMigrations: Remove unused param in test setup * CloudMigrations: Increase timeout of eventual checks and add debug message --- .../cloudmigrationimpl/cloudmigration_test.go | 56 ++++++++++--------- .../snapshot_mgmt_alerts_test.go | 18 +++--- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index e430611f3cd..f30feca4ee2 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -14,12 +14,10 @@ import ( "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/tracing" @@ -63,7 +61,7 @@ func Test_NoopServiceDoesNothing(t *testing.T) { func Test_CreateGetAndDeleteToken(t *testing.T) { t.Parallel() - s := setUpServiceTest(t, false) + s := setUpServiceTest(t) createResp, err := s.CreateToken(context.Background()) assert.NoError(t, err) @@ -88,7 +86,7 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { t.Parallel() setupTest := func(ctx context.Context) (service *Service, snapshotUID string, sessionUID string) { - s := setUpServiceTest(t, false).(*Service) + s := setUpServiceTest(t).(*Service) gmsClientFake := &gmsClientMock{} s.gmsClient = gmsClientFake @@ -365,7 +363,7 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) { func Test_OnlyQueriesStatusFromGMSWhenRequired(t *testing.T) { t.Parallel() - s := setUpServiceTest(t, false).(*Service) + s := setUpServiceTest(t).(*Service) gmsClientMock := &gmsClientMock{ getSnapshotResponse: &cloudmigration.GetSnapshotStatusResponse{ @@ -427,14 +425,29 @@ func Test_OnlyQueriesStatusFromGMSWhenRequired(t *testing.T) { Status: status, }) assert.NoError(t, err) - _, err := s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ + snapshot, err := s.GetSnapshot(context.Background(), cloudmigration.GetSnapshotsQuery{ SnapshotUID: uid, SessionUID: sess.UID, }) assert.NoError(t, err) - require.Eventually(t, func() bool { return gmsClientMock.GetSnapshotStatusCallCount() == i+1 }, time.Second, 10*time.Millisecond) + assert.Equal(t, status, snapshot.Status) + + require.Eventually( + t, + func() bool { return gmsClientMock.GetSnapshotStatusCallCount() == i+1 }, + 2*time.Second, + 100*time.Millisecond, + "GMS client mock GetSnapshotStatus count: %d", gmsClientMock.GetSnapshotStatusCallCount(), + ) } - assert.Never(t, func() bool { return gmsClientMock.GetSnapshotStatusCallCount() > 2 }, time.Second, 10*time.Millisecond) + + assert.Never( + t, + func() bool { return gmsClientMock.GetSnapshotStatusCallCount() > 2 }, + 2*time.Second, + 100*time.Millisecond, + "GMS client mock GetSnapshotStatus called more than expected: %d times", gmsClientMock.GetSnapshotStatusCallCount(), + ) } // Implementation inspired by ChatGPT, OpenAI's language model. @@ -463,7 +476,7 @@ func Test_SortFolders(t *testing.T) { func TestDeleteSession(t *testing.T) { t.Parallel() - s := setUpServiceTest(t, false).(*Service) + s := setUpServiceTest(t).(*Service) user := &user.SignedInUser{UserUID: "user123"} t.Run("when deleting a session that does not exist in the database, it returns an error", func(t *testing.T) { @@ -515,7 +528,7 @@ func TestReportEvent(t *testing.T) { gmsMock := &gmsClientMock{} - s := setUpServiceTest(t, false).(*Service) + s := setUpServiceTest(t).(*Service) s.gmsClient = gmsMock require.NotPanics(t, func() { @@ -533,7 +546,7 @@ func TestReportEvent(t *testing.T) { gmsMock := &gmsClientMock{} - s := setUpServiceTest(t, false).(*Service) + s := setUpServiceTest(t).(*Service) s.gmsClient = gmsMock require.NotPanics(t, func() { @@ -547,7 +560,7 @@ func TestReportEvent(t *testing.T) { func TestGetFolderNamesForFolderUIDs(t *testing.T) { t.Parallel() - s := setUpServiceTest(t, false).(*Service) + s := setUpServiceTest(t).(*Service) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) @@ -616,7 +629,7 @@ func TestGetParentNames(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - s := setUpServiceTest(t, false).(*Service) + s := setUpServiceTest(t).(*Service) user := &user.SignedInUser{OrgID: 1} @@ -705,7 +718,7 @@ func TestGetParentNames(t *testing.T) { func TestGetLibraryElementsCommands(t *testing.T) { t.Parallel() - s := setUpServiceTest(t, false).(*Service) + s := setUpServiceTest(t).(*Service) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) @@ -771,7 +784,7 @@ func TestIsPublicSignatureType(t *testing.T) { func TestGetPlugins(t *testing.T) { t.Parallel() - s := setUpServiceTest(t, false).(*Service) + s := setUpServiceTest(t).(*Service) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) @@ -869,7 +882,7 @@ func TestGetPlugins(t *testing.T) { type configOverrides func(c *setting.Cfg) -func setUpServiceTest(t *testing.T, withDashboardMock bool, cfgOverrides ...configOverrides) cloudmigration.Service { +func setUpServiceTest(t *testing.T, cfgOverrides ...configOverrides) cloudmigration.Service { secretsService := secretsfakes.NewFakeSecretsService() rr := routing.NewRouteRegister() tracer := tracing.InitializeTracerForTest() @@ -888,17 +901,6 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool, cfgOverrides ...conf cfg.CloudMigration.SnapshotFolder = filepath.Join(os.TempDir(), uuid.NewString()) dashboardService := dashboards.NewFakeDashboardService(t) - if withDashboardMock { - dashboardService.On("GetAllDashboards", mock.Anything).Return( - []*dashboards.Dashboard{ - { - UID: "1", - Data: simplejson.New(), - }, - }, - nil, - ) - } dsService := &datafakes.FakeDataSourceService{ DataSources: []*datasources.DataSource{ diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go index b382035c150..151d55562f9 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go @@ -45,7 +45,7 @@ func TestGetAlertMuteTimings(t *testing.T) { t.Run("it returns the mute timings", func(t *testing.T) { t.Parallel() - s := setUpServiceTest(t, false).(*Service) + s := setUpServiceTest(t).(*Service) s.features = featuremgmt.WithFeatures(featuremgmt.FlagOnPremToCloudMigrations) user := &user.SignedInUser{OrgID: 1} @@ -69,7 +69,7 @@ func TestGetNotificationTemplates(t *testing.T) { t.Run("it returns the notification templates", func(t *testing.T) { t.Parallel() - s := setUpServiceTest(t, false).(*Service) + s := setUpServiceTest(t).(*Service) user := &user.SignedInUser{OrgID: 1} @@ -92,7 +92,7 @@ func TestGetContactPoints(t *testing.T) { t.Run("it returns the contact points", func(t *testing.T) { t.Parallel() - s := setUpServiceTest(t, false).(*Service) + s := setUpServiceTest(t).(*Service) user := &user.SignedInUser{ OrgID: 1, @@ -115,7 +115,7 @@ func TestGetContactPoints(t *testing.T) { t.Run("it returns an error when user lacks permission to read contact point secrets", func(t *testing.T) { t.Parallel() - s := setUpServiceTest(t, false).(*Service) + s := setUpServiceTest(t).(*Service) user := &user.SignedInUser{ OrgID: 1, @@ -144,7 +144,7 @@ func TestGetNotificationPolicies(t *testing.T) { t.Run("it returns the contact points", func(t *testing.T) { t.Parallel() - s := setUpServiceTest(t, false).(*Service) + s := setUpServiceTest(t).(*Service) user := &user.SignedInUser{OrgID: 1} @@ -172,7 +172,7 @@ func TestGetAlertRules(t *testing.T) { t.Run("it returns the alert rules", func(t *testing.T) { t.Parallel() - s := setUpServiceTest(t, false).(*Service) + s := setUpServiceTest(t).(*Service) user := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: alertRulesPermissions}} @@ -191,7 +191,7 @@ func TestGetAlertRules(t *testing.T) { c.CloudMigration.AlertRulesState = setting.GMSAlertRulesPaused } - s := setUpServiceTest(t, false, alertRulesState).(*Service) + s := setUpServiceTest(t, alertRulesState).(*Service) user := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: alertRulesPermissions}} @@ -218,7 +218,7 @@ func TestGetAlertRuleGroups(t *testing.T) { t.Run("it returns the alert rule groups", func(t *testing.T) { t.Parallel() - s := setUpServiceTest(t, false).(*Service) + s := setUpServiceTest(t).(*Service) user := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: alertRulesPermissions}} @@ -257,7 +257,7 @@ func TestGetAlertRuleGroups(t *testing.T) { c.CloudMigration.AlertRulesState = setting.GMSAlertRulesPaused } - s := setUpServiceTest(t, false, alertRulesState).(*Service) + s := setUpServiceTest(t, alertRulesState).(*Service) user := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: alertRulesPermissions}} From 82d6fe5914045346d966a810876559d991b04ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 6 Oct 2025 10:34:16 +0200 Subject: [PATCH 006/578] Docs: Update language description in plugin schema (#112040) --- docs/sources/developers/plugins/plugin.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/developers/plugins/plugin.schema.json b/docs/sources/developers/plugins/plugin.schema.json index 3645aed2ef4..8cb3954f13d 100644 --- a/docs/sources/developers/plugins/plugin.schema.json +++ b/docs/sources/developers/plugins/plugin.schema.json @@ -688,7 +688,7 @@ }, "languages": { "type": "array", - "description": "The list of languages supported by the plugin. Each entry should be a locale identifier in the format `language-COUNTRY` (for example `en-US`, `fr-FR`, `es-ES`).", + "description": "The list of languages supported by the plugin. Each entry should be a locale identifier in the format `language-COUNTRY` (for example `en-US`, `es-ES`, `de-DE`).", "items": { "type": "string" } From 975c3b3f584ac3c21ece1ca892622da9ec03f8b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 6 Oct 2025 11:12:13 +0200 Subject: [PATCH 007/578] Chore: removes localizationForPlugins feature toggle (#111726) --- packages/grafana-data/src/types/featureToggles.gen.ts | 4 ---- 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 ++- 5 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 21fe50934cb..546b77a1af5 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -970,10 +970,6 @@ export interface FeatureToggles { */ multiTenantTempCredentials?: boolean; /** - * Enables localization for plugins - */ - localizationForPlugins?: boolean; - /** * Enables unified navbars * @default false */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 9be34ab40c1..beca14a83cc 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1677,13 +1677,6 @@ var ( HideFromDocs: true, Owner: awsDatasourcesSquad, }, - { - Name: "localizationForPlugins", - Description: "Enables localization for plugins", - Stage: FeatureStageExperimental, - Owner: grafanaPluginsPlatformSquad, - FrontendOnly: false, - }, { Name: "unifiedNavbars", Description: "Enables unified navbars", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 5f8f337c885..ea46bf6e069 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -217,7 +217,6 @@ unifiedStorageGrpcConnectionPool,experimental,@grafana/search-and-storage,false, alertingRulePermanentlyDelete,GA,@grafana/alerting-squad,false,false,true alertingRuleRecoverDeleted,GA,@grafana/alerting-squad,false,false,true multiTenantTempCredentials,experimental,@grafana/aws-datasources,false,false,false -localizationForPlugins,experimental,@grafana/plugins-platform-backend,false,false,false unifiedNavbars,GA,@grafana/plugins-platform-backend,false,false,true logsPanelControls,preview,@grafana/observability-logs,false,false,true metricsFromProfiles,experimental,@grafana/observability-traces-and-profiling,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index cd03ada2338..b22fc6ec5eb 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -878,10 +878,6 @@ const ( // use multi-tenant path for awsTempCredentials FlagMultiTenantTempCredentials = "multiTenantTempCredentials" - // FlagLocalizationForPlugins - // Enables localization for plugins - FlagLocalizationForPlugins = "localizationForPlugins" - // FlagUnifiedNavbars // Enables unified navbars FlagUnifiedNavbars = "unifiedNavbars" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 38b80c8554b..ac862d2312c 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2270,7 +2270,8 @@ "metadata": { "name": "localizationForPlugins", "resourceVersion": "1753448760331", - "creationTimestamp": "2025-03-31T04:38:38Z" + "creationTimestamp": "2025-03-31T04:38:38Z", + "deletionTimestamp": "2025-09-29T07:10:59Z" }, "spec": { "description": "Enables localization for plugins", From 2d801eed3cbe6618da710806235ca6d1f90b2963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Mon, 6 Oct 2025 11:47:52 +0200 Subject: [PATCH 008/578] Remove unused arguments from ResourceIndex and SearchBackend interfaces. (#112043) * Remove unused arguments from ResourceIndex and SearchBackend interfaces. --- pkg/storage/unified/resource/search.go | 35 ++++++------------ pkg/storage/unified/resource/search_test.go | 36 +++++++++---------- pkg/storage/unified/search/bleve.go | 8 ++--- pkg/storage/unified/search/bleve_test.go | 30 +++++++--------- pkg/storage/unified/testing/search_backend.go | 8 ++--- 5 files changed, 47 insertions(+), 70 deletions(-) diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index bc85a0a5a8a..50541be922f 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -88,7 +88,7 @@ type ResourceIndex interface { // UpdateIndex updates the index with the latest data (using update function provided when index was built) to guarantee strong consistency during the search. // Returns RV to which index was updated. - UpdateIndex(ctx context.Context, reason string) (int64, error) + UpdateIndex(ctx context.Context) (int64, error) // BuildInfo returns build information about the index. BuildInfo() (IndexBuildInfo, error) @@ -102,7 +102,7 @@ type UpdateFn func(context context.Context, index ResourceIndex, sinceRV int64) // SearchBackend contains the technology specific logic to support search type SearchBackend interface { // GetIndex returns existing index, or nil. - GetIndex(ctx context.Context, key NamespacedResource) (ResourceIndex, error) + GetIndex(key NamespacedResource) ResourceIndex // BuildIndex builds an index from scratch. // Depending on the size, the backend may choose different options (eg: memory vs disk). @@ -540,23 +540,18 @@ func (s *searchSupport) runPeriodicScanForIndexesToRebuild(ctx context.Context) s.log.Info("stopping periodic index rebuild due to context cancellation") return case <-ticker.C: - s.findIndexesToRebuild(ctx, time.Now()) + s.findIndexesToRebuild(time.Now()) } } } -func (s *searchSupport) findIndexesToRebuild(ctx context.Context, now time.Time) { +func (s *searchSupport) findIndexesToRebuild(now time.Time) { // Check all open indexes and see if any of them need to be rebuilt. // This is done periodically to make sure that the indexes are up to date. keys := s.search.GetOpenIndexes() for _, key := range keys { - idx, err := s.search.GetIndex(ctx, key) - if err != nil { - s.log.Error("failed to check index to rebuild", "key", key, "error", err) - continue - } - + idx := s.search.GetIndex(key) if idx == nil { // This can happen if index was closed in the meantime. continue @@ -618,13 +613,7 @@ func (s *searchSupport) rebuildIndex(ctx context.Context, req rebuildRequest) { l := s.log.With("namespace", req.Namespace, "group", req.Group, "resource", req.Resource) - idx, err := s.search.GetIndex(ctx, req.NamespacedResource) - if err != nil { - span.RecordError(err) - l.Error("failed to get index to rebuild", "error", err) - return - } - + idx := s.search.GetIndex(req.NamespacedResource) if idx == nil { span.AddEvent("index not found") l.Error("index not found") @@ -716,11 +705,7 @@ func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedReso attribute.String("namespace", key.Namespace), ) - idx, err := s.search.GetIndex(ctx, key) - if err != nil { - return nil, tracing.Error(span, err) - } - + idx := s.search.GetIndex(key) if idx == nil { span.AddEvent("Building index") ch := s.buildIndex.DoChan(key.String(), func() (interface{}, error) { @@ -730,8 +715,8 @@ func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedReso // Recheck if some other goroutine managed to build an index in the meantime. // (That is, it finished running this function and stored the index into the cache) - idx, err := s.search.GetIndex(ctx, key) - if err == nil && idx != nil { + idx := s.search.GetIndex(key) + if idx != nil { return idx, nil } @@ -773,7 +758,7 @@ func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedReso span.AddEvent("Updating index") start := time.Now() - rv, err := idx.UpdateIndex(ctx, reason) + rv, err := idx.UpdateIndex(ctx) if err != nil { return nil, tracing.Error(span, fmt.Errorf("failed to update index to guarantee strong consistency: %w", err)) } diff --git a/pkg/storage/unified/resource/search_test.go b/pkg/storage/unified/resource/search_test.go index 270719af2a7..34c9ee6927c 100644 --- a/pkg/storage/unified/resource/search_test.go +++ b/pkg/storage/unified/resource/search_test.go @@ -31,7 +31,7 @@ type MockResourceIndex struct { updateIndexError error updateIndexMu sync.Mutex - updateIndexCalls []string + updateIndexCalls int buildInfo IndexBuildInfo } @@ -65,11 +65,11 @@ func (m *MockResourceIndex) ListManagedObjects(ctx context.Context, req *resourc return args.Get(0).(*resourcepb.ListManagedObjectsResponse), args.Error(1) } -func (m *MockResourceIndex) UpdateIndex(ctx context.Context, reason string) (int64, error) { +func (m *MockResourceIndex) UpdateIndex(_ context.Context) (int64, error) { m.updateIndexMu.Lock() defer m.updateIndexMu.Unlock() - m.updateIndexCalls = append(m.updateIndexCalls, reason) + m.updateIndexCalls++ return 0, m.updateIndexError } @@ -144,10 +144,10 @@ type buildIndexCall struct { fields SearchableDocumentFields } -func (m *mockSearchBackend) GetIndex(ctx context.Context, key NamespacedResource) (ResourceIndex, error) { +func (m *mockSearchBackend) GetIndex(key NamespacedResource) ResourceIndex { m.mu.Lock() defer m.mu.Unlock() - return m.cache[key], nil + return m.cache[key] } func (m *mockSearchBackend) BuildIndex(ctx context.Context, key NamespacedResource, size int64, fields SearchableDocumentFields, reason string, builder BuildFn, updater UpdateFn, rebuild bool) (ResourceIndex, error) { @@ -271,24 +271,24 @@ func TestSearchGetOrCreateIndexWithIndexUpdate(t *testing.T) { idx, err := support.getOrCreateIndex(context.Background(), NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}, "initial call") require.NoError(t, err) require.NotNil(t, idx) - checkMockIndexUpdateCalls(t, idx, []string{"initial call"}) + checkMockIndexUpdateCalls(t, idx, 1) idx, err = support.getOrCreateIndex(context.Background(), NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}, "second call") require.NoError(t, err) require.NotNil(t, idx) - checkMockIndexUpdateCalls(t, idx, []string{"initial call", "second call"}) + checkMockIndexUpdateCalls(t, idx, 2) idx, err = support.getOrCreateIndex(context.Background(), NamespacedResource{Namespace: "ns", Group: "group", Resource: "bad"}, "call to bad index") require.ErrorIs(t, err, failedErr) require.Nil(t, idx) } -func checkMockIndexUpdateCalls(t *testing.T, idx ResourceIndex, strings []string) { +func checkMockIndexUpdateCalls(t *testing.T, idx ResourceIndex, calls int) { mi, ok := idx.(*MockResourceIndex) require.True(t, ok) mi.updateIndexMu.Lock() defer mi.updateIndexMu.Unlock() - require.Equal(t, strings, mi.updateIndexCalls) + require.Equal(t, calls, mi.updateIndexCalls) } func TestSearchGetOrCreateIndexWithCancellation(t *testing.T) { @@ -333,8 +333,8 @@ func TestSearchGetOrCreateIndexWithCancellation(t *testing.T) { // Wait until new index is put into cache. require.Eventually(t, func() bool { - idx, err := support.search.GetIndex(ctx, key) - return err == nil && idx != nil + idx := support.search.GetIndex(key) + return idx != nil }, 1*time.Second, 100*time.Millisecond, "Indexing finishes despite context cancellation") // Second call to getOrCreateIndex returns index immediately, even if context is canceled, as the index is now ready and cached. @@ -347,10 +347,10 @@ type slowSearchBackendWithCache struct { wg sync.WaitGroup } -func (m *slowSearchBackendWithCache) GetIndex(ctx context.Context, key NamespacedResource) (ResourceIndex, error) { +func (m *slowSearchBackendWithCache) GetIndex(key NamespacedResource) ResourceIndex { m.mu.Lock() defer m.mu.Unlock() - return m.cache[key], nil + return m.cache[key] } func (m *slowSearchBackendWithCache) BuildIndex(ctx context.Context, key NamespacedResource, size int64, fields SearchableDocumentFields, reason string, builder BuildFn, updater UpdateFn, rebuild bool) (ResourceIndex, error) { @@ -573,14 +573,14 @@ func TestFindIndexesForRebuild(t *testing.T) { require.NoError(t, err) require.NotNil(t, support) - support.findIndexesToRebuild(context.Background(), now) + support.findIndexesToRebuild(now) require.Equal(t, 6, support.rebuildQueue.Len()) now5m := now.Add(5 * time.Minute) // Running findIndexesToRebuild again should not add any new indexes to the rebuild queue, and all existing // ones should be "combined" with new ones (this will "bump" minBuildTime) - support.findIndexesToRebuild(context.Background(), now5m) + support.findIndexesToRebuild(now5m) require.Equal(t, 6, support.rebuildQueue.Len()) // Values that we expect to find in rebuild requests. @@ -692,8 +692,7 @@ func TestRebuildIndexes(t *testing.T) { func checkRebuildIndex(t *testing.T, support *searchSupport, req rebuildRequest, indexExists, expectedRebuild bool) { ctx := context.Background() - idxBefore, err := support.search.GetIndex(ctx, req.NamespacedResource) - require.NoError(t, err) + idxBefore := support.search.GetIndex(req.NamespacedResource) if indexExists { require.NotNil(t, idxBefore, "index should exist before rebuildIndex") } else { @@ -702,8 +701,7 @@ func checkRebuildIndex(t *testing.T, support *searchSupport, req rebuildRequest, support.rebuildIndex(ctx, req) - idxAfter, err := support.search.GetIndex(ctx, req.NamespacedResource) - require.NoError(t, err) + idxAfter := support.search.GetIndex(req.NamespacedResource) if indexExists { require.NotNil(t, idxAfter, "index should exist after rebuildIndex") diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index d8ac41e1332..190322bab7c 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -170,13 +170,13 @@ func NewBleveBackend(opts BleveOptions, tracer trace.Tracer, indexMetrics *resou } // GetIndex will return nil if the key does not exist -func (b *bleveBackend) GetIndex(_ context.Context, key resource.NamespacedResource) (resource.ResourceIndex, error) { +func (b *bleveBackend) GetIndex(key resource.NamespacedResource) resource.ResourceIndex { idx := b.getCachedIndex(key, time.Now()) // Avoid returning typed nils. if idx == nil { - return nil, nil + return nil } - return idx, nil + return idx } func (b *bleveBackend) GetOpenIndexes() []resource.NamespacedResource { @@ -1358,7 +1358,7 @@ func (b *bleveIndex) stopUpdaterAndCloseIndex() error { return b.index.Close() } -func (b *bleveIndex) UpdateIndex(ctx context.Context, reason string) (int64, error) { +func (b *bleveIndex) UpdateIndex(ctx context.Context) (int64, error) { // We don't have to do anything if the index cannot be updated (typically in tests). if b.updaterFn == nil { return 0, nil diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index 2c767e079df..c0ad9c68fc0 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -903,8 +903,7 @@ func TestBuildIndexExpiration(t *testing.T) { backend.runEvictExpiredOrUnownedIndexes(time.Now().Add(5 * time.Minute)) if tc.expectedEviction { - idx, err := backend.GetIndex(context.Background(), ns) - require.NoError(t, err) + idx := backend.GetIndex(ns) require.Nil(t, idx) _, err = builtIndex.DocCount(context.Background(), "") @@ -913,8 +912,7 @@ func TestBuildIndexExpiration(t *testing.T) { // Verify that there are no open indexes. checkOpenIndexes(t, reg, 0, 0) } else { - idx, err := backend.GetIndex(context.Background(), ns) - require.NoError(t, err) + idx := backend.GetIndex(ns) require.NotNil(t, idx) cnt, err := builtIndex.DocCount(context.Background(), "") @@ -1246,7 +1244,7 @@ func TestIndexUpdate(t *testing.T) { require.Equal(t, int64(0), resp.TotalHits) // Update index. - _, err = idx.UpdateIndex(context.Background(), "test") + _, err = idx.UpdateIndex(context.Background()) require.NoError(t, err) // Verify that index was updated -- number of docs didn't change, but we can search "gen_1" documents now. @@ -1254,7 +1252,7 @@ func TestIndexUpdate(t *testing.T) { require.Equal(t, int64(5), searchTitle(t, idx, "gen_1", 10, ns).TotalHits) // Update index again. - _, err = idx.UpdateIndex(context.Background(), "test") + _, err = idx.UpdateIndex(context.Background()) require.NoError(t, err) // Verify that index was updated again -- we can search "gen_2" now. "gen_1" documents are gone. require.Equal(t, 10, docCount(t, idx)) @@ -1298,13 +1296,13 @@ func TestConcurrentIndexUpdateAndBuildIndex(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - _, err = idx.UpdateIndex(ctx, "test") + _, err = idx.UpdateIndex(ctx) require.NoError(t, err) _, err = be.BuildIndex(t.Context(), ns, 10 /* file based */, nil, "test", indexTestDocs(ns, 10, 100), updaterFn, false) require.NoError(t, err) - _, err = idx.UpdateIndex(ctx, "test") + _, err = idx.UpdateIndex(ctx) require.Contains(t, err.Error(), bleve.ErrorIndexClosed.Error()) } @@ -1340,10 +1338,8 @@ func TestConcurrentIndexUpdateSearchAndRebuild(t *testing.T) { case <-time.After(time.Duration(i) * time.Millisecond): // introduce small jitter } - idx, err := be.GetIndex(ctx, ns) - require.NoError(t, err) // GetIndex doesn't really return error. - - _, err = idx.UpdateIndex(ctx, "test") + idx := be.GetIndex(ns) + _, err = idx.UpdateIndex(ctx) if err != nil { if errors.Is(err, bleve.ErrorIndexClosed) || errors.Is(err, context.Canceled) { continue @@ -1424,7 +1420,7 @@ func TestConcurrentIndexUpdateAndSearch(t *testing.T) { prevRV := int64(0) for ctx.Err() == nil { // We use t.Context() here to avoid getting errors from context cancellation. - rv, err := idx.UpdateIndex(t.Context(), "test") + rv, err := idx.UpdateIndex(t.Context()) require.NoError(t, err) require.Greater(t, rv, prevRV) // Each update should return new RV (that's how our update function works) require.Equal(t, int64(10), searchTitle(t, idx, "Document", 10, ns).TotalHits) @@ -1484,7 +1480,7 @@ func TestConcurrentIndexUpdateAndSearchWithIndexMinUpdateInterval(t *testing.T) attemptedUpdates.Inc() // We use t.Context() here to avoid getting errors from context cancellation. - rv, err := idx.UpdateIndex(t.Context(), "test") + rv, err := idx.UpdateIndex(t.Context()) require.NoError(t, err) // Our update function returns unix timestamp in millis. We expect it to not change at all, or change by minInterval. @@ -1536,7 +1532,7 @@ func TestIndexUpdateWithErrors(t *testing.T) { require.NoError(t, err) t.Run("update fail", func(t *testing.T) { - _, err = idx.UpdateIndex(t.Context(), "test") + _, err = idx.UpdateIndex(t.Context()) require.ErrorIs(t, err, updateErr) }) @@ -1544,7 +1540,7 @@ func TestIndexUpdateWithErrors(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) defer cancel() - _, err = idx.UpdateIndex(ctx, "test") + _, err = idx.UpdateIndex(ctx) require.ErrorIs(t, err, context.DeadlineExceeded) }) @@ -1553,7 +1549,7 @@ func TestIndexUpdateWithErrors(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() - _, err = idx.UpdateIndex(ctx, "test") + _, err = idx.UpdateIndex(ctx) require.ErrorIs(t, err, context.Canceled) }) } diff --git a/pkg/storage/unified/testing/search_backend.go b/pkg/storage/unified/testing/search_backend.go index ed1cffd5194..4ef4b6d2753 100644 --- a/pkg/storage/unified/testing/search_backend.go +++ b/pkg/storage/unified/testing/search_backend.go @@ -59,12 +59,11 @@ func runTestSearchBackendBuildIndex(t *testing.T, backend resource.SearchBackend } // Get the index should return nil if the index does not exist - index, err := backend.GetIndex(ctx, ns) - require.NoError(t, err) + index := backend.GetIndex(ns) require.Nil(t, index) // Build the index - index, err = backend.BuildIndex(ctx, ns, 0, nil, "test", func(index resource.ResourceIndex) (int64, error) { + index, err := backend.BuildIndex(ctx, ns, 0, nil, "test", func(index resource.ResourceIndex) (int64, error) { // Write a test document err := index.BulkIndex(&resource.BulkIndexRequest{ Items: []*resource.BulkIndexItem{ @@ -91,8 +90,7 @@ func runTestSearchBackendBuildIndex(t *testing.T, backend resource.SearchBackend require.NotNil(t, index) // Get the index should now return the index - index, err = backend.GetIndex(ctx, ns) - require.NoError(t, err) + index = backend.GetIndex(ns) require.NotNil(t, index) } From d6e362ade32c655abc3105b634d7661bc200f453 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Mon, 6 Oct 2025 11:50:02 +0200 Subject: [PATCH 009/578] Server: Add possibility to register build-specific targets (#111988) * Server: Add possibility to register Enterprise targets * wip authz service * Restore vscode * Better comment * Better comment v2' --- pkg/server/module_registerer.go | 20 ++++++++++++++++++++ pkg/server/module_server.go | 11 ++++++++++- pkg/server/search_server_distributor_test.go | 2 +- pkg/server/wire_gen.go | 3 ++- pkg/server/wireexts_oss.go | 2 ++ 5 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 pkg/server/module_registerer.go diff --git a/pkg/server/module_registerer.go b/pkg/server/module_registerer.go new file mode 100644 index 00000000000..8a459588937 --- /dev/null +++ b/pkg/server/module_registerer.go @@ -0,0 +1,20 @@ +package server + +import ( + "github.com/grafana/grafana/pkg/modules" +) + +// ModuleRegisterer is used to inject enterprise dskit modules into +// the module manager. This abstraction allows other builds (e.g. enterprise) to register +// additional modules while keeping the core server decoupled from build-specific dependencies. +type ModuleRegisterer interface { + RegisterModules(manager modules.Registry) +} + +type noopModuleRegisterer struct{} + +func (noopModuleRegisterer) RegisterModules(manager modules.Registry) {} + +func ProvideNoopModuleRegisterer() ModuleRegisterer { + return &noopModuleRegisterer{} +} diff --git a/pkg/server/module_server.go b/pkg/server/module_server.go index 5c1517d354d..5c420b9d219 100644 --- a/pkg/server/module_server.go +++ b/pkg/server/module_server.go @@ -44,8 +44,9 @@ func NewModule(opts Options, promGatherer prometheus.Gatherer, tracer tracing.Tracer, // Ensures tracing is initialized license licensing.Licensing, + moduleRegisterer ModuleRegisterer, ) (*ModuleServer, error) { - s, err := newModuleServer(opts, apiOpts, features, cfg, storageMetrics, indexMetrics, reg, promGatherer, license) + s, err := newModuleServer(opts, apiOpts, features, cfg, storageMetrics, indexMetrics, reg, promGatherer, license, moduleRegisterer) if err != nil { return nil, err } @@ -66,6 +67,7 @@ func newModuleServer(opts Options, reg prometheus.Registerer, promGatherer prometheus.Gatherer, license licensing.Licensing, + moduleRegisterer ModuleRegisterer, ) (*ModuleServer, error) { rootCtx, shutdownFn := context.WithCancel(context.Background()) @@ -87,6 +89,7 @@ func newModuleServer(opts Options, promGatherer: promGatherer, registerer: reg, license: license, + moduleRegisterer: moduleRegisterer, } return s, nil @@ -124,6 +127,9 @@ type ModuleServer struct { httpServerRouter *mux.Router searchServerRing *ring.Ring searchServerRingClientPool *ringclient.Pool + + // moduleRegisterer allows registration of modules provided by other builds (e.g. enterprise). + moduleRegisterer ModuleRegisterer } // init initializes the server and its services. @@ -202,6 +208,9 @@ func (s *ModuleServer) Run() error { m.RegisterModule(modules.All, nil) + // Register modules provided by other builds (e.g. enterprise). + s.moduleRegisterer.RegisterModules(m) + return m.Run(s.context) } diff --git a/pkg/server/search_server_distributor_test.go b/pkg/server/search_server_distributor_test.go index 18b5431ed89..e7304876688 100644 --- a/pkg/server/search_server_distributor_test.go +++ b/pkg/server/search_server_distributor_test.go @@ -326,7 +326,7 @@ func initModuleServerForTest( ) testModuleServer { tracer := tracing.InitializeTracerForTest() - ms, err := NewModule(opts, apiOpts, featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearch), cfg, nil, nil, prometheus.NewRegistry(), prometheus.DefaultGatherer, tracer, nil) + ms, err := NewModule(opts, apiOpts, featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearch), cfg, nil, nil, prometheus.NewRegistry(), prometheus.DefaultGatherer, tracer, nil, ProvideNoopModuleRegisterer()) require.NoError(t, err) conn, err := grpc.NewClient(cfg.GRPCServer.Address, diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index bcbb569df18..005ee68b230 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -1621,7 +1621,8 @@ func InitializeModuleServer(cfg *setting.Cfg, opts Options, apiOpts api.ServerOp } hooksService := hooks.ProvideService() ossLicensingService := licensing.ProvideService(cfg, hooksService) - moduleServer, err := NewModule(opts, apiOpts, featureToggles, cfg, storageMetrics, bleveIndexMetrics, registerer, gatherer, tracingService, ossLicensingService) + moduleRegisterer := ProvideNoopModuleRegisterer() + moduleServer, err := NewModule(opts, apiOpts, featureToggles, cfg, storageMetrics, bleveIndexMetrics, registerer, gatherer, tracingService, ossLicensingService, moduleRegisterer) if err != nil { return nil, err } diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 5ba3b26347f..6b2163f4e34 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -191,6 +191,8 @@ var wireExtsModuleServerSet = wire.NewSet( // Unified storage resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, + // Overriden by enterprise + ProvideNoopModuleRegisterer, ) var wireExtsStandaloneAPIServerSet = wire.NewSet( From 2ad00f99bffbdbd45450d6879d4b7072e8b4cf08 Mon Sep 17 00:00:00 2001 From: sudoice <143772207+sudoice@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:44:56 +0530 Subject: [PATCH 010/578] Transformations: Hide "Match all/any" conditions for less than two filters (#109754) Hide "Match all/any" conditions for less than two filters --- .../FilterByValueTransformerEditor.test.tsx | 59 +++++++++++++++++++ .../FilterByValueTransformerEditor.tsx | 18 +++--- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.test.tsx b/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.test.tsx index 6c064b2ad0c..89895799e6b 100644 --- a/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.test.tsx +++ b/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.test.tsx @@ -67,3 +67,62 @@ describe('FilterByValueTransformerEditor', () => { }); }); }); +it('hides conditions field when there is 0 or 1 filter', () => { + const onChangeMock = jest.fn(); + const input: DataFrame[] = [ + { + fields: [{ name: 'field1', type: FieldType.string, config: {}, values: [] }], + length: 0, + }, + ]; + + // Test with 0 filters + const { queryByText, rerender } = render( + + ); + expect(queryByText('Conditions')).not.toBeInTheDocument(); + + // Test with 1 filter + rerender( + + ); + expect(queryByText('Conditions')).not.toBeInTheDocument(); +}); + +it('shows conditions field when there are more than 1 filter', () => { + const onChangeMock = jest.fn(); + const input: DataFrame[] = [ + { + fields: [{ name: 'field1', type: FieldType.string, config: {}, values: [] }], + length: 0, + }, + ]; + + const { getByText } = render( + + ); + expect(getByText('Conditions')).toBeInTheDocument(); +}); diff --git a/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.tsx b/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.tsx index 366da659cf2..5f8c350f60f 100644 --- a/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.tsx +++ b/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.tsx @@ -124,14 +124,16 @@ export const FilterByValueTransformerEditor = (props: TransformerUIProps - -
- -
-
+ {options.filters.length > 1 && ( + +
+ +
+
+ )} {options.filters.map((filter, idx) => ( Date: Mon, 6 Oct 2025 13:29:33 +0200 Subject: [PATCH 011/578] Dashboard migrations: Fix `span: 0` bug and panel ordering in v16 dashboard migration (#112051) Fix span: 0 bug and panel ordering in v16 dashboard migration Fix span: 0 handling to match frontend behavior by defaulting to DEFAULT_PANEL_SPAN. Fix panel ordering issue by using stable sort instead of unstable sort. Fix collapsed property handling to only set when input row has collapse property. Add comprehensive test cases for span: 0 bug and collapsed property behavior. Add sanitized test input file for span: 0 demo dashboard with generic values instead of internal Grafana infrastructure references. All backend migration tests and frontend comparison tests pass. --- .../pkg/migration/frontend_defaults.go | 4 +- .../pkg/migration/schemaversion/v16.go | 63 +- .../pkg/migration/schemaversion/v16_test.go | 117 +++ .../testdata/input/v16.span_zero_demo.json | 687 ++++++++++++++ .../v16.span_zero_demo.v42.json | 881 ++++++++++++++++++ .../v16.span_zero_demo.v16.json | 694 ++++++++++++++ 6 files changed, 2414 insertions(+), 32 deletions(-) create mode 100644 apps/dashboard/pkg/migration/testdata/input/v16.span_zero_demo.json create mode 100644 apps/dashboard/pkg/migration/testdata/output/latest_version/v16.span_zero_demo.v42.json create mode 100644 apps/dashboard/pkg/migration/testdata/output/single_version/v16.span_zero_demo.v16.json diff --git a/apps/dashboard/pkg/migration/frontend_defaults.go b/apps/dashboard/pkg/migration/frontend_defaults.go index 6cce349a338..6f4cbbf87a4 100644 --- a/apps/dashboard/pkg/migration/frontend_defaults.go +++ b/apps/dashboard/pkg/migration/frontend_defaults.go @@ -198,7 +198,7 @@ func sortPanelsByGridPos(dashboard map[string]interface{}) { return } - sort.Slice(panels, func(i, j int) bool { + sort.SliceStable(panels, func(i, j int) bool { panelA := panels[i] panelB := panels[j] @@ -831,7 +831,7 @@ func cleanupPanelList(panels []interface{}) { // sortPanelsByGridPosition sorts panels by grid position (matches frontend sortPanelsByGridPos behavior) func sortPanelsByGridPosition(panels []interface{}) { - sort.Slice(panels, func(i, j int) bool { + sort.SliceStable(panels, func(i, j int) bool { panelA, okA := panels[i].(map[string]interface{}) panelB, okB := panels[j].(map[string]interface{}) if !okA || !okB { diff --git a/apps/dashboard/pkg/migration/schemaversion/v16.go b/apps/dashboard/pkg/migration/schemaversion/v16.go index d8fb357c3ef..da6db1dcc1a 100644 --- a/apps/dashboard/pkg/migration/schemaversion/v16.go +++ b/apps/dashboard/pkg/migration/schemaversion/v16.go @@ -49,10 +49,15 @@ func upgradeToGridLayout(dashboard map[string]interface{}) { maxPanelID := getMaxPanelID(rows) nextRowID := maxPanelID + 1 - // Get existing panels - var finalPanels []interface{} - if existingPanels, ok := dashboard["panels"].([]interface{}); ok { - finalPanels = existingPanels + // Match frontend: dashboard.panels already exists with top-level panels + // The frontend's this.dashboard.panels is initialized in the constructor with existing panels + // Then upgradeToGridLayout adds more panels to it + + // Initialize panels array - make a copy to avoid modifying the original + panels := []interface{}{} + if existingPanels, ok := dashboard["panels"].([]interface{}); ok && len(existingPanels) > 0 { + // Copy existing panels to preserve order + panels = append(panels, existingPanels...) } // Add special "row" panels if even one row is collapsed, repeated or has visible title (line 1028 in TS) @@ -72,7 +77,14 @@ func upgradeToGridLayout(dashboard map[string]interface{}) { height := getRowHeight(row) rowGridHeight := getGridHeight(height) - isCollapsed := GetBoolValue(row, "collapse") + // Check if collapse property exists and get its value + collapseValue, hasCollapseProperty := row["collapse"] + isCollapsed := false + if hasCollapseProperty { + if b, ok := collapseValue.(bool); ok { + isCollapsed = b + } + } var rowPanel map[string]interface{} @@ -110,9 +122,9 @@ func upgradeToGridLayout(dashboard map[string]interface{}) { }, } - // Set collapsed property only if the original row had a collapse property - // This matches the frontend behavior: rowPanel.collapsed = row.collapse - if _, hasCollapse := row["collapse"]; hasCollapse { + // Match frontend behavior: rowPanel.collapsed = row.collapse (line 1065 in TS) + // Only set collapsed property if the original row had a collapse property + if hasCollapseProperty { rowPanel["collapsed"] = isCollapsed } nextRowID++ @@ -128,20 +140,14 @@ func upgradeToGridLayout(dashboard map[string]interface{}) { continue } - // Check if panel already has gridPos but no valid span - // If span is missing or zero, and gridPos exists, preserve gridPos dimensions - var panelWidth, panelHeight int + // Match frontend logic: panel.span = panel.span || DEFAULT_PANEL_SPAN (line 1082 in TS) span := GetFloatValue(panel, "span", 0) - existingGridPos, hasGridPos := panel["gridPos"].(map[string]interface{}) - - if hasGridPos && span == 0 { - // Panel already has gridPos but no valid span - preserve its dimensions - panelWidth = GetIntValue(existingGridPos, "w", int(defaultPanelSpan*widthFactor)) - panelHeight = GetIntValue(existingGridPos, "h", rowGridHeight) - } else { - panelWidth, panelHeight = calculatePanelDimensionsFromSpan(span, panel, widthFactor, rowGridHeight) + if span == 0 { + span = defaultPanelSpan } + panelWidth, panelHeight := calculatePanelDimensionsFromSpan(span, panel, widthFactor, rowGridHeight) + panelPos := rowArea.getPanelPosition(panelHeight, panelWidth) yPos = rowArea.yPos @@ -157,21 +163,21 @@ func upgradeToGridLayout(dashboard map[string]interface{}) { // Remove span (line 1080 in TS) delete(panel, "span") - // Exact logic from lines 1082-1086 in TS + // Match frontend logic: lines 1101-1105 in TS if rowPanel != nil && isCollapsed { - // Add to collapsed row's nested panels + // Add to collapsed row's nested panels (line 1102) if rowPanelPanels, ok := rowPanel["panels"].([]interface{}); ok { rowPanel["panels"] = append(rowPanelPanels, panel) } } else { - // Add directly to dashboard panels - finalPanels = append(finalPanels, panel) + // Add directly to panels array like frontend (line 1104) + panels = append(panels, panel) } } - // Add row panel after processing all panels (lines 1089-1091 in TS) + // Add row panel after regular panels from this row (lines 1108-1110 in TS) if rowPanel != nil { - finalPanels = append(finalPanels, rowPanel) + panels = append(panels, rowPanel) } // Update yPos (lines 1093-1095 in TS) @@ -181,7 +187,7 @@ func upgradeToGridLayout(dashboard map[string]interface{}) { } // Update the dashboard - dashboard["panels"] = finalPanels + dashboard["panels"] = panels delete(dashboard, "rows") } @@ -313,10 +319,7 @@ func getGridHeight(height float64) int { } func calculatePanelDimensionsFromSpan(span float64, panel map[string]interface{}, widthFactor float64, defaultHeight int) (int, int) { - // Set default span if still 0 - if span == 0 { - span = defaultPanelSpan - } + // span should already be normalized by caller (line 1082 in DashboardMigrator.ts) if minSpan, hasMinSpan := panel["minSpan"]; hasMinSpan { if minSpanFloat, ok := ConvertToFloat(minSpan); ok && minSpanFloat > 0 { diff --git a/apps/dashboard/pkg/migration/schemaversion/v16_test.go b/apps/dashboard/pkg/migration/schemaversion/v16_test.go index 4ce1b423af3..007e912fc85 100644 --- a/apps/dashboard/pkg/migration/schemaversion/v16_test.go +++ b/apps/dashboard/pkg/migration/schemaversion/v16_test.go @@ -1532,6 +1532,123 @@ func TestV16(t *testing.T) { // rows field should be removed }, }, + { + name: "should handle span zero by defaulting to DEFAULT_PANEL_SPAN", + input: map[string]interface{}{ + "schemaVersion": 15, + "rows": []interface{}{ + map[string]interface{}{ + "collapse": false, + "showTitle": true, // Need this to create row panel + "title": "Test Row", + "height": 250, + "panels": []interface{}{ + map[string]interface{}{ + "id": 1, + "type": "graph", + "span": 0, // This should be defaulted to 4 (DEFAULT_PANEL_SPAN) + }, + map[string]interface{}{ + "id": 2, + "type": "stat", + "span": 6, // Normal span value + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "schemaVersion": 16, + "panels": []interface{}{ + map[string]interface{}{ + "id": 1, + "type": "graph", + "gridPos": map[string]interface{}{ + "x": 0, + "y": 1, + "w": 8, // span 0 -> DEFAULT_PANEL_SPAN (4) -> 4 * 2 = 8 width + "h": 7, // default height + }, + }, + map[string]interface{}{ + "id": 2, + "type": "stat", + "gridPos": map[string]interface{}{ + "x": 8, // After first panel + "y": 1, + "w": 12, // span 6 -> 6 * 2 = 12 width + "h": 7, // default height + }, + }, + // Row panel should be created because showTitle is true + map[string]interface{}{ + "id": 3, + "type": "row", + "title": "Test Row", + "collapsed": false, // Set because input has "collapse": false + "repeat": "", + "panels": []interface{}{}, + "gridPos": map[string]interface{}{ + "x": 0, + "y": 0, + "w": 24, + "h": 7, + }, + }, + }, + }, + }, + { + name: "should not set collapsed property when input row has no collapse property", + input: map[string]interface{}{ + "schemaVersion": 15, + "rows": []interface{}{ + map[string]interface{}{ + // No "collapse" property in input + "showTitle": true, + "title": "Test Row", + "height": 250, + "panels": []interface{}{ + map[string]interface{}{ + "id": 1, + "type": "graph", + "span": 12, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "schemaVersion": 16, + "panels": []interface{}{ + map[string]interface{}{ + "id": 1, + "type": "graph", + "gridPos": map[string]interface{}{ + "x": 0, + "y": 1, + "w": 24, // span 12 -> 12 * 2 = 24 width + "h": 7, // default height + }, + }, + // Row panel should be created because showTitle is true + map[string]interface{}{ + "id": 2, + "type": "row", + "title": "Test Row", + // No "collapsed" property because input had no "collapse" property + "repeat": "", + "panels": []interface{}{}, + "gridPos": map[string]interface{}{ + "x": 0, + "y": 0, + "w": 24, + "h": 7, + }, + }, + }, + }, + }, } runMigrationTests(t, tests, schemaversion.V16) diff --git a/apps/dashboard/pkg/migration/testdata/input/v16.span_zero_demo.json b/apps/dashboard/pkg/migration/testdata/input/v16.span_zero_demo.json new file mode 100644 index 00000000000..5dbb57fa244 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/input/v16.span_zero_demo.json @@ -0,0 +1,687 @@ +{ + "__requires": [ + { + "id": "grafana", + "name": "Grafana", + "type": "grafana", + "version": "8.0.0" + } + ], + "annotations": { + "list": [] + }, + "editable": false, + "gnetId": null, + "graphTooltip": 0, + "hideControls": false, + "links": [ + { + "icon": "external link", + "targetBlank": true, + "title": "External Documentation", + "type": "link", + "url": "https://example.com/docs" + } + ], + "panels": [ + { + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 0 + }, + "options": { + "content": "This dashboard demonstrates various monitoring components for application observability and performance metrics.\n", + "mode": "markdown" + }, + "title": "Application Monitoring", + "type": "text" + } + ], + "refresh": "10s", + "rows": [ + { + "collapse": false, + "collapsed": false, + "height": "250px", + "panels": [ + { + "gridPos": { + "h": 11, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 6, + "options": { + "content": "This service handles background processing tasks for the application system. It manages various types of operations including data synchronization, resource management, and batch processing.\n\nSupported operation types:\n1. Sync: Synchronizes data between different systems\n2. Process: Handles batch data processing tasks\n3. Cleanup: Removes outdated or temporary resources\n4. Update: Applies configuration changes across services\n\nService dependencies:\n- Data API: For reading and writing application data\n- Configuration Service: For managing system settings\n- Queue Service: For handling task scheduling\n- Storage Service: For persistent data management\n- Auth Service: For authentication and authorization\n- Metrics Service: For collecting operational statistics\n", + "mode": "markdown" + }, + "span": 0, + "title": "Service Overview", + "type": "text" + }, + { + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 7, + "options": { + "content": "Error monitoring helps identify issues in the system. This section displays error logs and success rates for operations.", + "mode": "markdown" + }, + "span": 0, + "title": "Error Monitoring", + "type": "text" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "yellow", + "value": 0.95 + }, + { + "color": "green", + "value": 1 + } + ] + }, + "unit": "percentunit" + } + }, + "gridPos": { + "h": 9, + "w": 3, + "x": 0, + "y": 19 + }, + "id": 8, + "span": 0, + "targets": [ + { + "expr": "sum by (action) (app_jobs_processed_total{outcome=\"success\", cluster=\"$cluster\", namespace=\"default\"})\n/\nsum by (action) (app_jobs_processed_total{cluster=\"$cluster\", namespace=\"default\"})\n", + "legendFormat": "{{action}}" + } + ], + "title": "Job Success Rate", + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "gridPos": { + "h": 9, + "w": 10, + "x": 3, + "y": 19 + }, + "id": 9, + "options": { + "enableLogDetails": true, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": true + }, + "span": 0, + "targets": [ + { + "expr": "{namespace=\"default\", cluster=\"$cluster\", job=\"app-service\"} | logfmt | level=\"error\"" + } + ], + "title": "Errors", + "type": "logs" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "gridPos": { + "h": 9, + "w": 11, + "x": 13, + "y": 19 + }, + "id": 10, + "options": { + "enableLogDetails": true, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": true + }, + "span": 0, + "targets": [ + { + "expr": "{namespace=\"default\", cluster=\"$cluster\", job=\"app-service\"} | logfmt" + } + ], + "title": "All", + "type": "logs" + }, + { + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 28 + }, + "id": 11, + "options": { + "content": "Performance monitoring examines factors that affect system response times, including operation duration, queue lengths, and processing delays. This section provides metrics and traces for performance analysis.\n", + "mode": "markdown" + }, + "span": 0, + "title": "Performance Analysis", + "type": "text" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Number of concurrent processing threads available for handling operations", + "gridPos": { + "h": 6, + "w": 5, + "x": 0, + "y": 31 + }, + "id": 12, + "span": 0, + "targets": [ + { + "expr": "max(app_worker_threads_active{cluster=\"$cluster\", namespace=\"default\"})", + "instant": true + } + ], + "title": "Concurrent Job Drivers", + "type": "stat" + }, + { + "datasource": { + "type": "tempo", + "uid": "${tempo}" + }, + "gridPos": { + "h": 6, + "w": 19, + "x": 5, + "y": 31 + }, + "id": 13, + "span": 0, + "targets": [ + { + "filters": [ + { + "id": "span-name", + "operator": "=", + "scope": "span", + "tag": "name", + "value": [ + "provisioning.sync.process" + ] + }, + { + "id": "k8s-cluster-name", + "operator": "=", + "scope": "resource", + "tag": "k8s.cluster.name", + "value": [ + "$cluster" + ] + } + ], + "query": "{name=\"app.operation.process\"}", + "queryType": "traceqlSearch" + } + ], + "title": "Recent Operation Traces", + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Histogram showing p99, p95, p50, and p10 percentiles for job processing duration based on number of resources changed", + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "yellow", + "value": 2 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "s" + } + }, + "gridPos": { + "h": 10, + "w": 8, + "x": 0, + "y": 55 + }, + "id": 14, + "span": 0, + "targets": [ + { + "expr": "histogram_quantile(0.99, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) > 0", + "legendFormat": "{{action}} q0.99 - size {{resources_changed_bucket}}", + "refId": "B" + }, + { + "expr": "histogram_quantile(0.9, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) > 0", + "legendFormat": "{{action}} q0.95 - size {{resources_changed_bucket}}", + "refId": "C" + }, + { + "expr": "histogram_quantile(0.5, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) > 0", + "legendFormat": "{{action}} q0.5 - size {{resources_changed_bucket}}", + "refId": "D" + }, + { + "expr": "histogram_quantile(0.1, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) > 0", + "legendFormat": "{{action}} q0.1 - size {{resources_changed_bucket}}", + "refId": "E" + } + ], + "timeFrom": "7d", + "title": "7d avg of job durations", + "transformations": [ + { + "id": "reduce", + "options": { + "mode": "seriesToRows", + "reducers": [ + "mean" + ] + } + }, + { + "id": "seriesToRows" + }, + { + "id": "organize", + "options": { + "renameByName": { + "Field": "Type", + "Mean": "Avg Duration", + "Metric": "Legend", + "Value": "Duration" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Histogram showing p99, p95, p50, and p10 percentiles for job processing duration based on number of resources changed", + "gridPos": { + "h": 10, + "w": 16, + "x": 8, + "y": 55 + }, + "id": 15, + "span": 0, + "targets": [ + { + "expr": "histogram_quantile(0.99, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.99 - size {{resources_changed_bucket}}", + "refId": "B" + }, + { + "expr": "histogram_quantile(0.95, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.95 - size {{resources_changed_bucket}}", + "refId": "C" + }, + { + "expr": "histogram_quantile(0.5, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.5 - size {{resources_changed_bucket}}", + "refId": "D" + }, + { + "expr": "histogram_quantile(0.1, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.1 - size {{resources_changed_bucket}}", + "refId": "E" + } + ], + "title": "Job Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Total number of jobs waiting to be processed", + "gridPos": { + "h": 5, + "w": 4, + "x": 0, + "y": 65 + }, + "id": 16, + "span": 0, + "targets": [ + { + "expr": "clamp_min(sum(app_operation_queue_size{cluster=\"$cluster\", namespace=\"default\"}), 0)", + "legendFormat": "Queue size" + } + ], + "title": "Queue Size", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "unit": "s" + } + }, + "gridPos": { + "h": 5, + "w": 4, + "x": 4, + "y": 65 + }, + "id": 17, + "span": 0, + "targets": [ + { + "expr": "avg(histogram_quantile(0.5, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le)))", + "legendFormat": "Queue size" + } + ], + "timeFrom": "7d", + "title": "7d avg Queue Wait Time", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "How long a job is in the queue before being picked up", + "gridPos": { + "h": 5, + "w": 16, + "x": 8, + "y": 65 + }, + "id": 18, + "span": 0, + "targets": [ + { + "expr": "histogram_quantile(0.99, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.99", + "refId": "B" + }, + { + "expr": "histogram_quantile(0.95, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.95", + "refId": "C" + }, + { + "expr": "histogram_quantile(0.5, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.5", + "refId": "D" + }, + { + "expr": "histogram_quantile(0.1, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.1", + "refId": "E" + } + ], + "title": "Queue Wait Time", + "type": "timeseries" + }, + { + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 52 + }, + "id": 19, + "options": { + "content": "Resource utilization monitoring for application containers", + "mode": "markdown" + }, + "span": 0, + "title": "Resource Monitoring", + "type": "text" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "gridPos": { + "h": 9, + "w": 7, + "x": 0, + "y": 55 + }, + "id": 20, + "span": 0, + "targets": [ + { + "expr": "count by (cluster, channel)(label_replace(label_replace(kube_pod_container_info{namespace=\"default\", container=\"app-worker\", pod=~\"app-worker.*\", cluster=~\"$cluster\"}, \"version\", \"$1\", \"image\", \".+:(.+)\"), \"channel\", \"$1\", \"container\", \".+-(.+)\"))", + "legendFormat": "{{cluster}}" + } + ], + "title": "Running Pod(s)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 7, + "y": 55 + }, + "id": 21, + "span": 0, + "targets": [ + { + "expr": "max(kube_pod_container_resource_requests{namespace=\"default\", resource=\"memory\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"})", + "legendFormat": "Memory Request" + }, + { + "expr": "max(kube_pod_container_resource_limits{namespace=\"default\", resource=\"memory\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"})", + "legendFormat": "Memory Limit" + }, + { + "expr": "max(container_memory_usage_bytes{namespace=\"default\",cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"}) by (pod)", + "legendFormat": "Container usage {{pod}}" + } + ], + "title": "Memory Utilization", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "gridPos": { + "h": 9, + "w": 9, + "x": 15, + "y": 55 + }, + "id": 22, + "span": 0, + "targets": [ + { + "expr": "sum(irate(container_cpu_usage_seconds_total{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\"}[$__rate_interval])) by (pod, container, cpu)", + "legendFormat": "Usage {{pod}}" + }, + { + "expr": "sum(irate(container_cpu_cfs_throttled_seconds_total{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\"}[$__rate_interval])) by (pod, container)", + "legendFormat": "Throttling {{pod}}" + }, + { + "expr": "max(kube_pod_container_resource_limits{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\", resource=\"cpu\"})", + "legendFormat": "CPU limit" + }, + { + "expr": "max(kube_pod_container_resource_requests{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\", resource=\"cpu\"})", + "legendFormat": "CPU request" + } + ], + "title": "CPU Utilization", + "type": "timeseries" + } + ], + "repeat": null, + "repeatIteration": null, + "repeatRowId": null, + "showTitle": true, + "title": "Application Service", + "titleSize": "h6" + } + ], + "schemaVersion": 15, + "style": "dark", + "tags": [ + "as-code" + ], + "templating": { + "list": [ + { + "current": { + "value": "prometheus-datasource" + }, + "hide": 0, + "label": "Data source", + "name": "datasource", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": { + "value": "prometheus-datasource" + }, + "name": "prom", + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": { + "value": "loki-datasource" + }, + "name": "loki", + "query": "loki", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": { + "text": "tempo-datasource", + "value": "tempo-datasource" + }, + "name": "tempo", + "query": "tempo", + "refresh": 1, + "regex": ".*tempo.*", + "type": "datasource" + }, + { + "current": { + "text": "demo-cluster", + "value": "demo-cluster" + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "name": "cluster", + "query": "label_values(app_worker_threads_active,cluster)", + "refresh": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "utc", + "title": "Span Zero Demo Dashboard", + "uid": "span-zero-demo-dashboard", + "version": 0 +} diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v16.span_zero_demo.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v16.span_zero_demo.v42.json new file mode 100644 index 00000000000..91012684ebf --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v16.span_zero_demo.v42.json @@ -0,0 +1,881 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": false, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [ + { + "icon": "external link", + "targetBlank": true, + "title": "External Documentation", + "type": "link", + "url": "https://example.com/docs" + } + ], + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "content": "This dashboard demonstrates various monitoring components for application observability and performance metrics.\n", + "mode": "markdown" + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Application Monitoring", + "type": "text" + }, + { + "collapsed": false, + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 23, + "panels": [], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Application Service", + "type": "row" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 1 + }, + "id": 6, + "options": { + "content": "This service handles background processing tasks for the application system. It manages various types of operations including data synchronization, resource management, and batch processing.\n\nSupported operation types:\n1. Sync: Synchronizes data between different systems\n2. Process: Handles batch data processing tasks\n3. Cleanup: Removes outdated or temporary resources\n4. Update: Applies configuration changes across services\n\nService dependencies:\n- Data API: For reading and writing application data\n- Configuration Service: For managing system settings\n- Queue Service: For handling task scheduling\n- Storage Service: For persistent data management\n- Auth Service: For authentication and authorization\n- Metrics Service: For collecting operational statistics\n", + "mode": "markdown" + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Service Overview", + "type": "text" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 1 + }, + "id": 7, + "options": { + "content": "Error monitoring helps identify issues in the system. This section displays error logs and success rates for operations.", + "mode": "markdown" + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Error Monitoring", + "type": "text" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "yellow", + "value": 0.95 + }, + { + "color": "green", + "value": 1 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 1 + }, + "id": 8, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "sum by (action) (app_jobs_processed_total{outcome=\"success\", cluster=\"$cluster\", namespace=\"default\"})\n/\nsum by (action) (app_jobs_processed_total{cluster=\"$cluster\", namespace=\"default\"})\n", + "legendFormat": "{{action}}", + "refId": "A" + } + ], + "title": "Job Success Rate", + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 8 + }, + "id": 9, + "options": { + "enableLogDetails": true, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": true + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "expr": "{namespace=\"default\", cluster=\"$cluster\", job=\"app-service\"} | logfmt | level=\"error\"", + "refId": "A" + } + ], + "title": "Errors", + "type": "logs" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 8 + }, + "id": 10, + "options": { + "enableLogDetails": true, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": true + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "expr": "{namespace=\"default\", cluster=\"$cluster\", job=\"app-service\"} | logfmt", + "refId": "A" + } + ], + "title": "All", + "type": "logs" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 8 + }, + "id": 11, + "options": { + "content": "Performance monitoring examines factors that affect system response times, including operation duration, queue lengths, and processing delays. This section provides metrics and traces for performance analysis.\n", + "mode": "markdown" + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Performance Analysis", + "type": "text" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Number of concurrent processing threads available for handling operations", + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 15 + }, + "id": 12, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "max(app_worker_threads_active{cluster=\"$cluster\", namespace=\"default\"})", + "instant": true, + "refId": "A" + } + ], + "title": "Concurrent Job Drivers", + "type": "stat" + }, + { + "datasource": { + "type": "tempo", + "uid": "${tempo}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 15 + }, + "id": 13, + "targets": [ + { + "datasource": { + "type": "tempo", + "uid": "${tempo}" + }, + "filters": [ + { + "id": "span-name", + "operator": "=", + "scope": "span", + "tag": "name", + "value": [ + "provisioning.sync.process" + ] + }, + { + "id": "k8s-cluster-name", + "operator": "=", + "scope": "resource", + "tag": "k8s.cluster.name", + "value": [ + "$cluster" + ] + } + ], + "query": "{name=\"app.operation.process\"}", + "queryType": "traceqlSearch", + "refId": "A" + } + ], + "title": "Recent Operation Traces", + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Histogram showing p99, p95, p50, and p10 percentiles for job processing duration based on number of resources changed", + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "yellow", + "value": 2 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 15 + }, + "id": 14, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.99, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.99 - size {{resources_changed_bucket}}", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.9, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.95 - size {{resources_changed_bucket}}", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.5, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.5 - size {{resources_changed_bucket}}", + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.1, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.1 - size {{resources_changed_bucket}}", + "refId": "E" + } + ], + "timeFrom": "7d", + "title": "7d avg of job durations", + "transformations": [ + { + "id": "reduce", + "options": { + "mode": "seriesToRows", + "reducers": [ + "mean" + ] + } + }, + { + "id": "seriesToRows" + }, + { + "id": "organize", + "options": { + "renameByName": { + "Field": "Type", + "Mean": "Avg Duration", + "Metric": "Legend", + "Value": "Duration" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Histogram showing p99, p95, p50, and p10 percentiles for job processing duration based on number of resources changed", + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 22 + }, + "id": 15, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.99, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.99 - size {{resources_changed_bucket}}", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.95, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.95 - size {{resources_changed_bucket}}", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.5, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.5 - size {{resources_changed_bucket}}", + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.1, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.1 - size {{resources_changed_bucket}}", + "refId": "E" + } + ], + "title": "Job Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Total number of jobs waiting to be processed", + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 22 + }, + "id": 16, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "clamp_min(sum(app_operation_queue_size{cluster=\"$cluster\", namespace=\"default\"}), 0)", + "legendFormat": "Queue size", + "refId": "A" + } + ], + "title": "Queue Size", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 22 + }, + "id": 17, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "avg(histogram_quantile(0.5, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le)))", + "legendFormat": "Queue size", + "refId": "A" + } + ], + "timeFrom": "7d", + "title": "7d avg Queue Wait Time", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "How long a job is in the queue before being picked up", + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 29 + }, + "id": 18, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.99, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.99", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.95, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.95", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.5, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.5", + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.1, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.1", + "refId": "E" + } + ], + "title": "Queue Wait Time", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 29 + }, + "id": 19, + "options": { + "content": "Resource utilization monitoring for application containers", + "mode": "markdown" + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Resource Monitoring", + "type": "text" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 29 + }, + "id": 20, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "count by (cluster, channel)(label_replace(label_replace(kube_pod_container_info{namespace=\"default\", container=\"app-worker\", pod=~\"app-worker.*\", cluster=~\"$cluster\"}, \"version\", \"$1\", \"image\", \".+:(.+)\"), \"channel\", \"$1\", \"container\", \".+-(.+)\"))", + "legendFormat": "{{cluster}}", + "refId": "A" + } + ], + "title": "Running Pod(s)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 36 + }, + "id": 21, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "max(kube_pod_container_resource_requests{namespace=\"default\", resource=\"memory\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"})", + "legendFormat": "Memory Request", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "max(kube_pod_container_resource_limits{namespace=\"default\", resource=\"memory\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"})", + "legendFormat": "Memory Limit", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "max(container_memory_usage_bytes{namespace=\"default\",cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"}) by (pod)", + "legendFormat": "Container usage {{pod}}", + "refId": "C" + } + ], + "title": "Memory Utilization", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 36 + }, + "id": 22, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "sum(irate(container_cpu_usage_seconds_total{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\"}[$__rate_interval])) by (pod, container, cpu)", + "legendFormat": "Usage {{pod}}", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "sum(irate(container_cpu_cfs_throttled_seconds_total{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\"}[$__rate_interval])) by (pod, container)", + "legendFormat": "Throttling {{pod}}", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "max(kube_pod_container_resource_limits{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\", resource=\"cpu\"})", + "legendFormat": "CPU limit", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "max(kube_pod_container_resource_requests{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\", resource=\"cpu\"})", + "legendFormat": "CPU request", + "refId": "D" + } + ], + "title": "CPU Utilization", + "type": "timeseries" + } + ], + "refresh": "10s", + "schemaVersion": 42, + "tags": [ + "as-code" + ], + "templating": { + "list": [ + { + "current": { + "value": "prometheus-datasource" + }, + "hide": 0, + "label": "Data source", + "name": "datasource", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": { + "value": "prometheus-datasource" + }, + "name": "prom", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": { + "value": "loki-datasource" + }, + "name": "loki", + "options": [], + "query": "loki", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": { + "text": "tempo-datasource", + "value": "tempo-datasource" + }, + "name": "tempo", + "options": [], + "query": "tempo", + "refresh": 1, + "regex": ".*tempo.*", + "type": "datasource" + }, + { + "current": { + "text": "demo-cluster", + "value": "demo-cluster" + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "name": "cluster", + "options": [], + "query": "label_values(app_worker_threads_active,cluster)", + "refresh": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "utc", + "title": "Span Zero Demo Dashboard", + "uid": "span-zero-demo-dashboard", + "weekStart": "" +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v16.span_zero_demo.v16.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v16.span_zero_demo.v16.json new file mode 100644 index 00000000000..089f4ac16d3 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v16.span_zero_demo.v16.json @@ -0,0 +1,694 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": false, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [ + { + "icon": "external link", + "targetBlank": true, + "title": "External Documentation", + "type": "link", + "url": "https://example.com/docs" + } + ], + "panels": [ + { + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "content": "This dashboard demonstrates various monitoring components for application observability and performance metrics.\n", + "mode": "markdown" + }, + "title": "Application Monitoring", + "type": "text" + }, + { + "collapsed": false, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 23, + "panels": [], + "title": "Application Service", + "type": "row" + }, + { + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 1 + }, + "id": 6, + "options": { + "content": "This service handles background processing tasks for the application system. It manages various types of operations including data synchronization, resource management, and batch processing.\n\nSupported operation types:\n1. Sync: Synchronizes data between different systems\n2. Process: Handles batch data processing tasks\n3. Cleanup: Removes outdated or temporary resources\n4. Update: Applies configuration changes across services\n\nService dependencies:\n- Data API: For reading and writing application data\n- Configuration Service: For managing system settings\n- Queue Service: For handling task scheduling\n- Storage Service: For persistent data management\n- Auth Service: For authentication and authorization\n- Metrics Service: For collecting operational statistics\n", + "mode": "markdown" + }, + "title": "Service Overview", + "type": "text" + }, + { + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 1 + }, + "id": 7, + "options": { + "content": "Error monitoring helps identify issues in the system. This section displays error logs and success rates for operations.", + "mode": "markdown" + }, + "title": "Error Monitoring", + "type": "text" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "yellow", + "value": 0.95 + }, + { + "color": "green", + "value": 1 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 1 + }, + "id": 8, + "targets": [ + { + "expr": "sum by (action) (app_jobs_processed_total{outcome=\"success\", cluster=\"$cluster\", namespace=\"default\"})\n/\nsum by (action) (app_jobs_processed_total{cluster=\"$cluster\", namespace=\"default\"})\n", + "legendFormat": "{{action}}", + "refId": "A" + } + ], + "title": "Job Success Rate", + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 8 + }, + "id": 9, + "options": { + "enableLogDetails": true, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": true + }, + "targets": [ + { + "expr": "{namespace=\"default\", cluster=\"$cluster\", job=\"app-service\"} | logfmt | level=\"error\"", + "refId": "A" + } + ], + "title": "Errors", + "type": "logs" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 8 + }, + "id": 10, + "options": { + "enableLogDetails": true, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": true + }, + "targets": [ + { + "expr": "{namespace=\"default\", cluster=\"$cluster\", job=\"app-service\"} | logfmt", + "refId": "A" + } + ], + "title": "All", + "type": "logs" + }, + { + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 8 + }, + "id": 11, + "options": { + "content": "Performance monitoring examines factors that affect system response times, including operation duration, queue lengths, and processing delays. This section provides metrics and traces for performance analysis.\n", + "mode": "markdown" + }, + "title": "Performance Analysis", + "type": "text" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Number of concurrent processing threads available for handling operations", + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 15 + }, + "id": 12, + "targets": [ + { + "expr": "max(app_worker_threads_active{cluster=\"$cluster\", namespace=\"default\"})", + "instant": true, + "refId": "A" + } + ], + "title": "Concurrent Job Drivers", + "type": "stat" + }, + { + "datasource": { + "type": "tempo", + "uid": "${tempo}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 15 + }, + "id": 13, + "targets": [ + { + "filters": [ + { + "id": "span-name", + "operator": "=", + "scope": "span", + "tag": "name", + "value": [ + "provisioning.sync.process" + ] + }, + { + "id": "k8s-cluster-name", + "operator": "=", + "scope": "resource", + "tag": "k8s.cluster.name", + "value": [ + "$cluster" + ] + } + ], + "query": "{name=\"app.operation.process\"}", + "queryType": "traceqlSearch", + "refId": "A" + } + ], + "title": "Recent Operation Traces", + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Histogram showing p99, p95, p50, and p10 percentiles for job processing duration based on number of resources changed", + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "yellow", + "value": 2 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 15 + }, + "id": 14, + "targets": [ + { + "expr": "histogram_quantile(0.99, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.99 - size {{resources_changed_bucket}}", + "refId": "B" + }, + { + "expr": "histogram_quantile(0.9, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.95 - size {{resources_changed_bucket}}", + "refId": "C" + }, + { + "expr": "histogram_quantile(0.5, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.5 - size {{resources_changed_bucket}}", + "refId": "D" + }, + { + "expr": "histogram_quantile(0.1, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.1 - size {{resources_changed_bucket}}", + "refId": "E" + } + ], + "timeFrom": "7d", + "title": "7d avg of job durations", + "transformations": [ + { + "id": "reduce", + "options": { + "mode": "seriesToRows", + "reducers": [ + "mean" + ] + } + }, + { + "id": "seriesToRows" + }, + { + "id": "organize", + "options": { + "renameByName": { + "Field": "Type", + "Mean": "Avg Duration", + "Metric": "Legend", + "Value": "Duration" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Histogram showing p99, p95, p50, and p10 percentiles for job processing duration based on number of resources changed", + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 22 + }, + "id": 15, + "targets": [ + { + "expr": "histogram_quantile(0.99, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.99 - size {{resources_changed_bucket}}", + "refId": "B" + }, + { + "expr": "histogram_quantile(0.95, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.95 - size {{resources_changed_bucket}}", + "refId": "C" + }, + { + "expr": "histogram_quantile(0.5, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.5 - size {{resources_changed_bucket}}", + "refId": "D" + }, + { + "expr": "histogram_quantile(0.1, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.1 - size {{resources_changed_bucket}}", + "refId": "E" + } + ], + "title": "Job Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Total number of jobs waiting to be processed", + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 22 + }, + "id": 16, + "targets": [ + { + "expr": "clamp_min(sum(app_operation_queue_size{cluster=\"$cluster\", namespace=\"default\"}), 0)", + "legendFormat": "Queue size", + "refId": "A" + } + ], + "title": "Queue Size", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 22 + }, + "id": 17, + "targets": [ + { + "expr": "avg(histogram_quantile(0.5, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le)))", + "legendFormat": "Queue size", + "refId": "A" + } + ], + "timeFrom": "7d", + "title": "7d avg Queue Wait Time", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "How long a job is in the queue before being picked up", + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 29 + }, + "id": 18, + "targets": [ + { + "expr": "histogram_quantile(0.99, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.99", + "refId": "B" + }, + { + "expr": "histogram_quantile(0.95, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.95", + "refId": "C" + }, + { + "expr": "histogram_quantile(0.5, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.5", + "refId": "D" + }, + { + "expr": "histogram_quantile(0.1, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.1", + "refId": "E" + } + ], + "title": "Queue Wait Time", + "type": "timeseries" + }, + { + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 29 + }, + "id": 19, + "options": { + "content": "Resource utilization monitoring for application containers", + "mode": "markdown" + }, + "title": "Resource Monitoring", + "type": "text" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 29 + }, + "id": 20, + "targets": [ + { + "expr": "count by (cluster, channel)(label_replace(label_replace(kube_pod_container_info{namespace=\"default\", container=\"app-worker\", pod=~\"app-worker.*\", cluster=~\"$cluster\"}, \"version\", \"$1\", \"image\", \".+:(.+)\"), \"channel\", \"$1\", \"container\", \".+-(.+)\"))", + "legendFormat": "{{cluster}}", + "refId": "A" + } + ], + "title": "Running Pod(s)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 36 + }, + "id": 21, + "targets": [ + { + "expr": "max(kube_pod_container_resource_requests{namespace=\"default\", resource=\"memory\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"})", + "legendFormat": "Memory Request", + "refId": "A" + }, + { + "expr": "max(kube_pod_container_resource_limits{namespace=\"default\", resource=\"memory\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"})", + "legendFormat": "Memory Limit", + "refId": "B" + }, + { + "expr": "max(container_memory_usage_bytes{namespace=\"default\",cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"}) by (pod)", + "legendFormat": "Container usage {{pod}}", + "refId": "C" + } + ], + "title": "Memory Utilization", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 36 + }, + "id": 22, + "targets": [ + { + "expr": "sum(irate(container_cpu_usage_seconds_total{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\"}[$__rate_interval])) by (pod, container, cpu)", + "legendFormat": "Usage {{pod}}", + "refId": "A" + }, + { + "expr": "sum(irate(container_cpu_cfs_throttled_seconds_total{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\"}[$__rate_interval])) by (pod, container)", + "legendFormat": "Throttling {{pod}}", + "refId": "B" + }, + { + "expr": "max(kube_pod_container_resource_limits{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\", resource=\"cpu\"})", + "legendFormat": "CPU limit", + "refId": "C" + }, + { + "expr": "max(kube_pod_container_resource_requests{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\", resource=\"cpu\"})", + "legendFormat": "CPU request", + "refId": "D" + } + ], + "title": "CPU Utilization", + "type": "timeseries" + } + ], + "refresh": "10s", + "schemaVersion": 16, + "tags": [ + "as-code" + ], + "templating": { + "list": [ + { + "current": { + "value": "prometheus-datasource" + }, + "hide": 0, + "label": "Data source", + "name": "datasource", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": { + "value": "prometheus-datasource" + }, + "name": "prom", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": { + "value": "loki-datasource" + }, + "name": "loki", + "options": [], + "query": "loki", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": { + "text": "tempo-datasource", + "value": "tempo-datasource" + }, + "name": "tempo", + "options": [], + "query": "tempo", + "refresh": 1, + "regex": ".*tempo.*", + "type": "datasource" + }, + { + "current": { + "text": "demo-cluster", + "value": "demo-cluster" + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "name": "cluster", + "options": [], + "query": "label_values(app_worker_threads_active,cluster)", + "refresh": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "utc", + "title": "Span Zero Demo Dashboard", + "uid": "span-zero-demo-dashboard", + "weekStart": "" +} \ No newline at end of file From 548e739d7543ffdefa83bba1daf837a5ff312ca9 Mon Sep 17 00:00:00 2001 From: Hugo Kiyodi Oshiro Date: Mon, 6 Oct 2025 08:46:47 -0300 Subject: [PATCH 012/578] Plugins: Add feature highlights tabs (#110712) --- .../core/components/Branding/OrangeBadge.tsx | 21 ++- .../app/core/components/Upgrade/ProBadge.tsx | 17 +- .../app/features/connections/Connections.tsx | 27 +++ .../components/FeatureHighlightsTabPage.tsx | 172 ++++++++++++++++++ .../connections/hooks/useDataSourceTabNav.ts | 95 ++++++++++ .../pages/CacheFeatureHighlightPage.tsx | 33 ++++ .../pages/InsightsFeatureHighlightPage.tsx | 40 ++++ .../pages/PermissionsFeatureHighlightPage.tsx | 36 ++++ .../tabs/ConnectData/DataSourceTabs.test.tsx | 150 +++++++++++++++ .../components/DataSourceTabPage.tsx | 2 +- .../features/datasources/state/navModel.ts | 29 ++- public/img/cache-screenshot.png | Bin 0 -> 292076 bytes public/img/insights-screenshot.png | Bin 0 -> 381572 bytes public/img/permissions-screenshot.png | Bin 0 -> 307421 bytes public/locales/en-US/grafana.json | 28 +++ 15 files changed, 621 insertions(+), 29 deletions(-) create mode 100644 public/app/features/connections/components/FeatureHighlightsTabPage.tsx create mode 100644 public/app/features/connections/hooks/useDataSourceTabNav.ts create mode 100644 public/app/features/connections/pages/CacheFeatureHighlightPage.tsx create mode 100644 public/app/features/connections/pages/InsightsFeatureHighlightPage.tsx create mode 100644 public/app/features/connections/pages/PermissionsFeatureHighlightPage.tsx create mode 100644 public/app/features/connections/tabs/ConnectData/DataSourceTabs.test.tsx create mode 100644 public/img/cache-screenshot.png create mode 100644 public/img/insights-screenshot.png create mode 100644 public/img/permissions-screenshot.png diff --git a/public/app/core/components/Branding/OrangeBadge.tsx b/public/app/core/components/Branding/OrangeBadge.tsx index be9d080df79..3088c63cec5 100644 --- a/public/app/core/components/Branding/OrangeBadge.tsx +++ b/public/app/core/components/Branding/OrangeBadge.tsx @@ -1,19 +1,25 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; +import { HTMLAttributes } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Icon, useStyles2 } from '@grafana/ui'; -export function OrangeBadge({ text }: { text: string }) { - const styles = useStyles2(getStyles); +interface Props extends HTMLAttributes { + text?: string; + className?: string; +} + +export function OrangeBadge({ text, className, ...htmlProps }: Props) { + const styles = useStyles2(getStyles, text); return ( -
+
{text}
); } -const getStyles = (theme: GrafanaTheme2) => { +const getStyles = (theme: GrafanaTheme2, text: string | undefined) => { return { wrapper: css({ display: 'inline-flex', @@ -26,6 +32,11 @@ const getStyles = (theme: GrafanaTheme2) => { fontSize: theme.typography.bodySmall.fontSize, lineHeight: theme.typography.bodySmall.lineHeight, alignItems: 'center', + ...(text === undefined && { + svg: { + marginRight: 0, + }, + }), }), }; }; diff --git a/public/app/core/components/Upgrade/ProBadge.tsx b/public/app/core/components/Upgrade/ProBadge.tsx index a7e9eec4b50..dc5aadf5d13 100644 --- a/public/app/core/components/Upgrade/ProBadge.tsx +++ b/public/app/core/components/Upgrade/ProBadge.tsx @@ -5,13 +5,14 @@ import { GrafanaTheme2 } from '@grafana/data'; import { reportExperimentView } from '@grafana/runtime'; import { useStyles2 } from '@grafana/ui'; +import { OrangeBadge } from '../Branding/OrangeBadge'; + export interface Props extends HTMLAttributes { - text?: string; experimentId?: string; eventVariant?: string; } -export const ProBadge = ({ text = 'PRO', className, experimentId, eventVariant = '', ...htmlProps }: Props) => { +export const ProBadge = ({ className, experimentId, eventVariant = '', ...htmlProps }: Props) => { const styles = useStyles2(getStyles); useEffect(() => { @@ -20,23 +21,13 @@ export const ProBadge = ({ text = 'PRO', className, experimentId, eventVariant = } }, [experimentId, eventVariant]); - return ( - - {text} - - ); + return ; }; const getStyles = (theme: GrafanaTheme2) => { return { badge: css({ marginLeft: theme.spacing(1.25), - borderRadius: theme.shape.borderRadius(5), - backgroundColor: theme.colors.success.main, - padding: theme.spacing(0.25, 0.75), - color: 'white', // use the same color for both themes - fontWeight: theme.typography.fontWeightMedium, - fontSize: theme.typography.pxToRem(10), }), }; }; diff --git a/public/app/features/connections/Connections.tsx b/public/app/features/connections/Connections.tsx index 6d40a294c55..4d40c5684b7 100644 --- a/public/app/features/connections/Connections.tsx +++ b/public/app/features/connections/Connections.tsx @@ -2,14 +2,19 @@ import { Navigate, Routes, Route, useLocation } from 'react-router-dom-v5-compat import { StoreState, useSelector } from 'app/types/store'; +import { isOpenSourceBuildOrUnlicenced } from '../admin/EnterpriseAuthFeaturesCard'; + import { ROUTES } from './constants'; import { AddNewConnectionPage } from './pages/AddNewConnectionPage'; +import { CacheFeatureHighlightPage } from './pages/CacheFeatureHighlightPage'; import ConnectionsHomePage from './pages/ConnectionsHomePage'; import { DataSourceDashboardsPage } from './pages/DataSourceDashboardsPage'; import { DataSourceDetailsPage } from './pages/DataSourceDetailsPage'; import { DataSourcesListPage } from './pages/DataSourcesListPage'; import { EditDataSourcePage } from './pages/EditDataSourcePage'; +import { InsightsFeatureHighlightPage } from './pages/InsightsFeatureHighlightPage'; import { NewDataSourcePage } from './pages/NewDataSourcePage'; +import { PermissionsFeatureHighlightPage } from './pages/PermissionsFeatureHighlightPage'; function RedirectToAddNewConnection() { const { search } = useLocation(); @@ -27,6 +32,7 @@ function RedirectToAddNewConnection() { export default function Connections() { const navIndex = useSelector((state: StoreState) => state.navIndex); const isAddNewConnectionPageOverridden = Boolean(navIndex['standalone-plugin-page-/connections/add-new-connection']); + const shouldEnableFeatureHighlights = isOpenSourceBuildOrUnlicenced(); return ( @@ -41,6 +47,27 @@ export default function Connections() { element={} /> } /> + + {shouldEnableFeatureHighlights && ( + <> + } + /> + } + /> + } + /> + + )} + (); + useInitDataSourceSettings(uid); + + const { navId, pageNav, dataSourceHeader } = useDataSourceTabNav(pageName); + const styles = useStyles2(getStyles); + + const info = useDataSourceInfo({ + dataSourcePluginName: pageNav.dataSourcePluginName, + alertingSupported: dataSourceHeader.alertingSupported, + }); + + return ( + } + info={info} + actions={} + > + +
+
+
+ +
+

{title}

+
{header}
+
+ {items.map((item) => ( +
+ + {item} +
+ ))} +
+
+ + Create a Grafana Cloud Free account to start using data source permissions. This feature is also + available with a Grafana Enterprise license. + +
+ + + Learn about Enterprise + +
+
+ + + Create account + +

+ + After creating an account, you can easily{' '} + + migrate this instance to Grafana Cloud + {' '} + with our Migration Assistant. + +

+
+
+ {`${pageName} +
+
+
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css({ + display: 'flex', + gap: theme.spacing(4), + alignItems: 'flex-start', + [theme.breakpoints.down('lg')]: { + flexDirection: 'column', + }, + }), + content: css({ + flex: '0 0 40%', + }), + imageContainer: css({ + flex: '0 0 60%', + display: 'flex', + [theme.breakpoints.down('lg')]: { + flex: '1 1 auto', + }, + padding: `${theme.spacing(5)} 10% 0 ${theme.spacing(5)}`, + }), + image: css({ + width: '100%', + borderRadius: theme.shape.radius.default, + boxShadow: theme.shadows.z3, + }), + buttonIcon: css({ + marginRight: theme.spacing(1), + }), + badge: css({ + marginBottom: theme.spacing(1), + }), + title: css({ + marginBottom: theme.spacing(2), + marginTop: theme.spacing(2), + }), + header: css({ + color: theme.colors.text.primary, + }), + + itemsList: css({ + marginBottom: theme.spacing(3), + marginTop: theme.spacing(3), + }), + + listItem: css({ + display: 'flex', + alignItems: 'flex-start', + color: theme.colors.text.primary, + lineHeight: theme.typography.bodySmall.lineHeight, + marginBottom: theme.spacing(2), + }), + + linkButton: css({ + marginBottom: theme.spacing(2), + }), + + footer: css({ + marginBottom: theme.spacing(3), + marginTop: theme.spacing(3), + }), + + icon: css({ + marginRight: theme.spacing(1), + color: theme.colors.success.main, + }), + footNote: css({ + color: theme.colors.text.secondary, + fontSize: theme.typography.bodySmall.fontSize, + }), +}); diff --git a/public/app/features/connections/hooks/useDataSourceTabNav.ts b/public/app/features/connections/hooks/useDataSourceTabNav.ts new file mode 100644 index 00000000000..2c7231330b5 --- /dev/null +++ b/public/app/features/connections/hooks/useDataSourceTabNav.ts @@ -0,0 +1,95 @@ +import { useLocation, useParams } from 'react-router-dom-v5-compat'; + +import { NavModel, NavModelItem } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { getDataSourceSrv } from '@grafana/runtime'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { useDataSource, useDataSourceMeta, useDataSourceSettings } from 'app/features/datasources/state/hooks'; +import { getDataSourceLoadingNav, buildNavModel, getDataSourceNav } from 'app/features/datasources/state/navModel'; +import { useGetSingle } from 'app/features/plugins/admin/state/hooks'; +import { useSelector } from 'app/types/store'; + +export function useDataSourceTabNav(pageName: string, pageIdParam?: string) { + const { uid = '' } = useParams<{ uid: string }>(); + const location = useLocation(); + const datasource = useDataSource(uid); + const dataSourceMeta = useDataSourceMeta(datasource.type); + const datasourcePlugin = useGetSingle(datasource.type); + const params = new URLSearchParams(location.search); + const pageId = pageIdParam || params.get('page'); + + const { plugin, loadError, loading } = useDataSourceSettings(); + const dsi = getDataSourceSrv()?.getInstanceSettings(uid); + const hasAlertingEnabled = Boolean(dsi?.meta?.alerting ?? false); + const isAlertManagerDatasource = dsi?.type === 'alertmanager'; + const alertingSupported = hasAlertingEnabled || isAlertManagerDatasource; + + const navIndex = useSelector((state) => state.navIndex); + const navIndexId = pageId ? `datasource-${pageId}-${uid}` : `datasource-${pageName}-${uid}`; + + let pageNav: NavModel = { + node: { + text: t('connections.use-data-source-settings-nav.page-nav.text.data-source-nav-node', 'Data Source Nav Node'), + }, + main: { + text: t('connections.use-data-source-settings-nav.page-nav.text.data-source-nav-node', 'Data Source Nav Node'), + }, + }; + + if (loadError) { + const node: NavModelItem = { + text: loadError, + subTitle: t('connections.use-data-source-settings-nav.node.subTitle.data-source-error', 'Data Source Error'), + icon: 'exclamation-triangle', + }; + + pageNav = { + node: node, + main: node, + }; + } + + if (loading || !plugin) { + pageNav = getNavModel(navIndex, navIndexId, getDataSourceLoadingNav(pageName)); + } + + if (!datasource.uid) { + const node: NavModelItem = { + text: t('connections.use-data-source-settings-nav.node.subTitle.data-source-error', 'Data Source Error'), + icon: 'exclamation-triangle', + }; + + pageNav = { + node: node, + main: node, + }; + } + + if (plugin) { + pageNav = getNavModel( + navIndex, + navIndexId, + getDataSourceNav(buildNavModel(datasource, plugin), pageId || pageName) + ); + } + + const connectionsPageNav = { + ...pageNav.main, + dataSourcePluginName: datasourcePlugin?.name || plugin?.meta.name || '', + active: true, + text: datasource.name || '', + subTitle: dataSourceMeta.name ? `Type: ${dataSourceMeta.name}` : '', + children: (pageNav.main.children || []).map((navModelItem) => ({ + ...navModelItem, + url: navModelItem.url?.replace('datasources/edit/', '/connections/datasources/edit/'), + })), + }; + + return { + navId: 'connections-datasources', + pageNav: connectionsPageNav, + dataSourceHeader: { + alertingSupported, + }, + }; +} diff --git a/public/app/features/connections/pages/CacheFeatureHighlightPage.tsx b/public/app/features/connections/pages/CacheFeatureHighlightPage.tsx new file mode 100644 index 00000000000..bb63af4e189 --- /dev/null +++ b/public/app/features/connections/pages/CacheFeatureHighlightPage.tsx @@ -0,0 +1,33 @@ +import { t } from '@grafana/i18n'; +import cacheScreenshot from 'img/cache-screenshot.png'; + +import { FeatureHighlightsTabPage } from '../components/FeatureHighlightsTabPage'; + +export function CacheFeatureHighlightPage() { + return ( + + ); +} diff --git a/public/app/features/connections/pages/InsightsFeatureHighlightPage.tsx b/public/app/features/connections/pages/InsightsFeatureHighlightPage.tsx new file mode 100644 index 00000000000..78f24a5026c --- /dev/null +++ b/public/app/features/connections/pages/InsightsFeatureHighlightPage.tsx @@ -0,0 +1,40 @@ +import { t } from '@grafana/i18n'; +import insightsScreenshot from 'img/insights-screenshot.png'; + +import { FeatureHighlightsTabPage } from '../components/FeatureHighlightsTabPage'; + +export function InsightsFeatureHighlightPage() { + return ( + + ); +} diff --git a/public/app/features/connections/pages/PermissionsFeatureHighlightPage.tsx b/public/app/features/connections/pages/PermissionsFeatureHighlightPage.tsx new file mode 100644 index 00000000000..4d5b8884b34 --- /dev/null +++ b/public/app/features/connections/pages/PermissionsFeatureHighlightPage.tsx @@ -0,0 +1,36 @@ +import { t } from '@grafana/i18n'; +import permissionsScreenshot from 'img/permissions-screenshot.png'; + +import { FeatureHighlightsTabPage } from '../components/FeatureHighlightsTabPage'; + +export function PermissionsFeatureHighlightPage() { + return ( + + ); +} diff --git a/public/app/features/connections/tabs/ConnectData/DataSourceTabs.test.tsx b/public/app/features/connections/tabs/ConnectData/DataSourceTabs.test.tsx new file mode 100644 index 00000000000..6a772be530b --- /dev/null +++ b/public/app/features/connections/tabs/ConnectData/DataSourceTabs.test.tsx @@ -0,0 +1,150 @@ +import { RenderResult, screen } from '@testing-library/react'; +import { Route, Routes } from 'react-router-dom-v5-compat'; +import { render } from 'test/test-utils'; + +import { LayoutModes, PluginType } from '@grafana/data'; +import { setPluginLinksHook, setPluginComponentsHook } from '@grafana/runtime'; +import { contextSrv } from 'app/core/services/context_srv'; +import * as api from 'app/features/datasources/api'; +import { getMockDataSources } from 'app/features/datasources/mocks/dataSourcesMocks'; +import { configureStore } from 'app/store/configureStore'; + +import { getPluginsStateMock } from '../../../plugins/admin/mocks/mockHelpers'; +import Connections from '../../Connections'; +import { ROUTES } from '../../constants'; +import { navIndex } from '../../mocks/store.navIndex.mock'; + +setPluginLinksHook(() => ({ links: [], isLoading: false })); +setPluginComponentsHook(() => ({ components: [], isLoading: false })); + +const mockDatasources = getMockDataSources(3); + +const renderPage = ( + path: string = ROUTES.Base, + store = configureStore({ + navIndex, + plugins: getPluginsStateMock([]), + dataSources: { + dataSources: mockDatasources, + dataSourcesCount: mockDatasources.length, + isLoadingDataSources: false, + searchQuery: '', + dataSourceTypeSearchQuery: '', + layoutMode: LayoutModes.List, + dataSource: mockDatasources[0], + dataSourceMeta: { + id: '', + name: '', + type: PluginType.panel, + info: { + author: { + name: '', + url: undefined, + }, + description: '', + links: [], + logos: { + large: '', + small: '', + }, + screenshots: [], + updated: '', + version: '', + }, + module: '', + baseUrl: '', + backend: true, + isBackend: true, + }, + isLoadingDataSourcePlugins: false, + plugins: [], + categories: [], + isSortAscending: true, + }, + }) +): RenderResult => { + return render( + + } /> + , + { + store, + historyOptions: { initialEntries: [path] }, + } + ); +}; + +jest.mock('@grafana/runtime', () => { + const original = jest.requireActual('@grafana/runtime'); + return { + ...original, + config: { + ...original.config, + bootData: { + user: { + orgId: 1, + timezone: 'UTC', + }, + navTree: [], + }, + featureToggles: { + ...original.config.featureToggles, + }, + datasources: {}, + defaultDatasource: '', + buildInfo: { + ...original.config.buildInfo, + edition: 'Open Source', + }, + caching: { + ...original.config.caching, + enabled: true, + }, + }, + getTemplateSrv: () => ({ + replace: (str: string) => str, + }), + getDataSourceSrv: () => { + return { + getInstanceSettings: (uid: string) => { + return { + id: uid, + uid: uid, + type: PluginType.datasource, + name: uid, + meta: { + id: uid, + name: uid, + type: PluginType.datasource, + backend: true, + isBackend: true, + }, + }; + }, + }; + }, + }; +}); + +describe('DataSourceEditTabs', () => { + beforeEach(() => { + process.env.NODE_ENV = 'test'; + (api.getDataSources as jest.Mock) = jest.fn().mockResolvedValue(mockDatasources); + (contextSrv.hasPermission as jest.Mock) = jest.fn().mockReturnValue(true); + }); + + it('should render Permissions and Insights tabs', () => { + const path = ROUTES.DataSourcesEdit.replace(':uid', mockDatasources[0].uid); + renderPage(path); + + const permissionsTab = screen.getByTestId('data-testid Tab Permissions'); + expect(permissionsTab).toBeInTheDocument(); + expect(permissionsTab).toHaveTextContent('Permissions'); + expect(permissionsTab).toHaveAttribute('href', '/connections/datasources/edit/x/permissions'); + + const insightsTab = screen.getByTestId('data-testid Tab Insights'); + expect(insightsTab).toBeInTheDocument(); + expect(insightsTab).toHaveTextContent('Insights'); + expect(insightsTab).toHaveAttribute('href', '/connections/datasources/edit/x/insights'); + }); +}); diff --git a/public/app/features/datasources/components/DataSourceTabPage.tsx b/public/app/features/datasources/components/DataSourceTabPage.tsx index 2857cebe084..03f43e400c5 100644 --- a/public/app/features/datasources/components/DataSourceTabPage.tsx +++ b/public/app/features/datasources/components/DataSourceTabPage.tsx @@ -13,7 +13,7 @@ export interface Props { } export function DataSourceTabPage({ uid, pageId }: Props) { - const { navId, pageNav, dataSourceHeader } = useDataSourceSettingsNav(); + const { navId, pageNav, dataSourceHeader } = useDataSourceSettingsNav('settings'); const info = useDataSourceInfo({ dataSourcePluginName: pageNav.dataSourcePluginName, diff --git a/public/app/features/datasources/state/navModel.ts b/public/app/features/datasources/state/navModel.ts index 2c685beae68..b11c13c6c4f 100644 --- a/public/app/features/datasources/state/navModel.ts +++ b/public/app/features/datasources/state/navModel.ts @@ -4,6 +4,7 @@ import { featureEnabled } from '@grafana/runtime'; import { ProBadge } from 'app/core/components/Upgrade/ProBadge'; import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; +import { isOpenSourceBuildOrUnlicenced } from 'app/features/admin/EnterpriseAuthFeaturesCard'; import { highlightTrial } from 'app/features/admin/utils'; import { AccessControlAction } from 'app/types/accessControl'; import icnDatasourceSvg from 'img/icn-datasource.svg'; @@ -53,6 +54,8 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat }); } + const shouldEnableFeatureHighlights = isOpenSourceBuildOrUnlicenced(); + const isLoadingNav = dataSource.type === loadingDSType; const permissionsExperimentId = 'feature-highlights-data-source-permissions-badge'; @@ -64,12 +67,15 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat url: `datasources/edit/${dataSource.uid}/permissions`, }; - if (highlightTrial() && !isLoadingNav) { + if ((highlightTrial() && !isLoadingNav) || shouldEnableFeatureHighlights) { dsPermissions.tabSuffix = () => ProBadge({ experimentId: permissionsExperimentId, eventVariant: 'trial' }); } - if (featureEnabled('dspermissions.enforcement')) { - if (contextSrv.hasPermissionInMetadata(AccessControlAction.DataSourcesPermissionsRead, dataSource)) { + if (featureEnabled('dspermissions.enforcement') || shouldEnableFeatureHighlights) { + if ( + contextSrv.hasPermissionInMetadata(AccessControlAction.DataSourcesPermissionsRead, dataSource) || + shouldEnableFeatureHighlights + ) { navModel.children!.push(dsPermissions); } } else if (highlightsEnabled && !isLoadingNav) { @@ -80,7 +86,7 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat }); } - if (config.analytics?.enabled) { + if (config.analytics?.enabled || shouldEnableFeatureHighlights) { const analyticsExperimentId = 'feature-highlights-data-source-insights-badge'; const analytics: NavModelItem = { active: false, @@ -90,12 +96,12 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat url: `datasources/edit/${dataSource.uid}/insights`, }; - if (highlightTrial() && !isLoadingNav) { + if ((highlightTrial() && !isLoadingNav) || shouldEnableFeatureHighlights) { analytics.tabSuffix = () => ProBadge({ experimentId: analyticsExperimentId, eventVariant: 'trial' }); } - if (featureEnabled('analytics')) { - if (contextSrv.hasPermission(AccessControlAction.DataSourcesInsightsRead)) { + if (featureEnabled('analytics') || shouldEnableFeatureHighlights) { + if (contextSrv.hasPermission(AccessControlAction.DataSourcesInsightsRead) || shouldEnableFeatureHighlights) { navModel.children!.push(analytics); } } else if (highlightsEnabled && !isLoadingNav) { @@ -118,12 +124,15 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat hideFromTabs: !pluginMeta.isBackend || !config.caching.enabled, }; - if (highlightTrial() && !isLoadingNav) { + if ((highlightTrial() && !isLoadingNav) || shouldEnableFeatureHighlights) { caching.tabSuffix = () => ProBadge({ experimentId: cachingExperimentId, eventVariant: 'trial' }); } - if (featureEnabled('caching')) { - if (contextSrv.hasPermissionInMetadata(AccessControlAction.DataSourcesCachingRead, dataSource)) { + if (featureEnabled('caching') || shouldEnableFeatureHighlights) { + if ( + contextSrv.hasPermissionInMetadata(AccessControlAction.DataSourcesCachingRead, dataSource) || + shouldEnableFeatureHighlights + ) { navModel.children!.push(caching); } } else if (highlightsEnabled && !isLoadingNav) { diff --git a/public/img/cache-screenshot.png b/public/img/cache-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..501b5c38f967b33b411b636f1ef95aa66a9535b5 GIT binary patch literal 292076 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Π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
  • F~Bk}xNd-%yePI|i|fNk6(&#{-}?5&xXbt@9P# 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

    %-4 zN`T(_vbw+652^L{TqnWIcWeiz#rBSI@<<~92*-7qBS3&6qN_|**=!J7GQ#)nIpyc# zXYFZ+{q8v9J&Yob-oH#7-S)O{cvge^6NQEyIyJZJ1P;@M+;SBJ!haawr`4}Z8#LG0 z9UpOaKd)W>Z7)BHbu{-wDso189zPsoVXf;kt{rt;xB`_>bda;Z2zh<{gX#;rIFyJ= zTx>9WG_e-sztr&Z$isczySYg>YnRf`)y_;HA#moFE+!ZxPqef)l8{)-&C@dOMvv5Z zfC>+(wi4(VL#9TbDn8LyGiapOZ-L{dpC#QUorKN*k0U@ZiSEfVWn*Hv=B?MA>jGM= zABXL1EcgDtzs+bAms6Beyx2Gvss1X^;^^yIzZz6zb_URbv>%f1p#;_WU#A7nth$+1 zXh6j-MDb40DPDm5{wQ`7_|lXVgn!7?xg>x7meYWe2{-j~>q5W~;m!1Nzvp+D$xI03 zkioR{7hU#Q)uZ-|phOx}l};JO83TVY0|E-i?HJH(DwDnAz+MNsM4j$LprK(K962|C`P{;|24ty-I_E=eZ>QT;zvY{u z)%O#K$5ctxavD^5y+N5tx?H+Q!dVsOUlF(CWCr`pcp*u--*G; z%gM{{zEyQCCDjKlu3{{oJx-wWz+y*4^6~^_lGKKj43O$Eg8r`YSySJmS?y$f5bUwfX^dJ9*vvq0)zpIeJ^T5yXE89| zVLzvTe=USwp%|Oxw$#N_&&%cqoO|>A^r|^jC5RAe%gv2u&l=^8Mk23QwS3;6a$Jka zi~45I>mBv&vqB#1LfeE%(D}&5VJH72|F?m}PHflx&nCHaGtAKWmOJ^A%iiy7yx5lH zOwxGESc#BXq4Ew}L890#_4Lq9_4Dl^5?0^vE_hz1wmWCLTsWYb@&Qv8-lM6rd)LCW z3xU5oD;CJ@KXFq*CNhsH_!hw&2J9(z#PiyMJf=K+c?z=IhqW9bHFn(PwLN;v%~3oB z7ZpjgS<^gEz*uxTZM{FJ!s{c$YBruwGu0uwHfDGfwgx=zj=Sez819ILR&9Q?L5miFdrj}NNma?Wq^)GsdPUyEn*9BH7Chq;Q* zU78t35SR#dr>I?(Zc0>07f4A&>GPnhBC7Uqk1%RZAgx!t*MId2x^_3n`g8SuI;QIJpJ! zy4o&qwnu9#+{#VZ;qxKxrhIyz-nZ^T+t_=3=`*g?vAk*85gv`Jg;`>3E+5{-)+23E zofw?%q{ddH+E(!OweGU|!X;t0kBk<@vbQX4s}Ja3T-0G99qfp0SS!$tMt3?h9S}Wr zzP-L$@hu1SJxV#g?yo-8uW`*QQrH88TXo&b>bajG;3)?@3KWB9nDz$z>2AitYJ9h5 zTS5_V*)Yfs5nXu|^2&92Ond8<K_!|2Qr zxft1K=DQh7>%X7W$E92^#%k;c(V*%%`}X4NVnN80>Js&>OLFmQ{gl-iJe^wq1kv|e zVPS`V`0X`I_AFy;k_)J{FsG5Sm|Z+GeLY^lS|&t-hCn2s*g?~Rt^1A_1HKtY)_zLC zr5Qo*{dMYk@;b!(o#so1=5oTY#HYwe6(>OHBC zP5XlV$m%r2_Cn7WFs;4k!q@(|eS>S#Oxd|A6jAQPX-kcGbEl}PSME)AqmAH<>PxB7 zsy6&EJO-T=QT*J|E3+5YMfnZkw$V3N&!3B0IW3G;ttj^1&DWQdgZpCJ-&P5_41Z_+6q>0pkE2|-JyX`@SmS$p@{~5`?{D548-wI#&Fyh&ghjwiwnFi)P`JH3CVcd>AN;ltv+bvY(? z-w$zRbjs0(q0ho;i6n?N(FvAx4nDH+ zcCQ(uK)W+chH5ISVq}86S`h!Pdt{Qog)gz2MI9lt656=b-nvnfU?78>UJ3cA;DZ6} zq>K6k#0O3~cxdQ3NB77$)jKX20>HC%W3&9)I7u!R50oN!TJJ3;wlXM&vfSHp`W*l@ zw)#H>>!JgQ(A^L*89u&64`QYCmCLV8>GYX`OQc^VXx^eyQEsfcA3T{WwQ6kqs;xCA z89UVT4>vsffHu1>Puc70n2Ft0-#GT83*MVm&@!dVkz!KD73n@6DSW53=dgUPSoo z!o5=<7$~U0so>Yg4oyCRpg(ZpHH6o{WX!(rZw2R@=j;s428O1^J}*8+E*XClEowpk z_4C@NJ<{CGjM-udyFm!?IJfvDZbqO4rqVhbUEpYp$DIrh`x5);!a)p z3#0||M*g`4`icKr(8r(8B&uKG;uy62;Kxcvg>}C(SeX+-PoY9xIM9afZ{1b4*p+Bi z*AeOc#x6(rI>N;9x_|8j*SCRZudfA6h`#x3jutY7*cf>d6?}Zv%$Oh)td2?lWXAO6 z-X_V!*PZ9qDHruM#-&1SrPZ(_&d@4G1ZNJ-2cxc5)PHkf@)Wyf!mqQ>*ZGg)r)b52o^|**O{^a6z(=Z3l+WB@$p_1$ILEghj zSW3&-w^NvOQa+7Yh&NRLuEb|iwDmX6_g`Hka|o}qE0W$)$mvDWfRgq;=Hy9=E&hP;XK!!9-hE51Wo=?zv;ieA-IJsr>zwYF^SKm+clq8?A zn-I+Gc@(KE?kt%kc*tl9%u;s##qs@`Wrgn7+sp1DGpV~z5%7E@QvJcFGPgO&t!RZH zZ`W#P68cVlua-szIloH5u-&f2ETf$J3qOtp7BKz{H(L9%6j=4yiK?7h+=vGcNh*@N zGhfzFk{9;^M^?1OJE^fxp|*D>y1F*%Yzbza8@I;zlf(BElJTM#^83!l%F$c^GASZ! z+!vRQh~fpst=R<4x?pmqrrN-LHm%B}-H0ST#B%=a{d3m?h{^A1+v|pLP0Bgb%cn`Az4SYC{~Pf5+tUt1-)RxB zIzLgbU>y*?zZddItRlqZOd0+F?|#G=Vgo-JjiL$t`hXW?S}r~Nnlp#yPD^)Eh-S}T z0AO{iZ{+O9J7=tPrimhW3MR+fLah+S#=3Gg_+;Nnj4t}`I{|Nbcne8GBX%}hlLU1E zx#8vUJscT3=5F9KC5!wSxfMhGsCLy#j~LvzS>g^~@Hr-~jPivlHr=N14XFrq-j5sz zdbeG`GM^KtP}!zk11vN7y8f_R=*hZU4=}@@W{)z?2qq*leTfhp%iI*K;|i4Qh8Pq$ z0_`Yn^d@Bw!CSHGVc*Iwc*deTF-#@1&Bj0Yg!DN@u%1FbHUh z%l4?7v2Twzw<@wo1Gnm*pWm55yuQ>kFDs-2 zPc(X{M!tLhO(3zl)0qHYKo^7b@g@ZCAfqrw#jSkg?eig?RI|E1Vumt!w}D^@@rh?e zw@d7()C$Za^A`J9+MT+5mUCM82a8t0n-^P^hNogoz>=>jsAgP(mfW$yf2a|j_=m^;ont0tCR2Mn}h%NTO&Nr6%wxX#TW$y2q56b24@b-ss#dHZ-kpR zLFeLa4S6VLGLem3ha-km+k0c(>^z^Mj;8DIQ?;s4&Us(1d?U;Izz=T#DtngcjtJy) zwZ#-e>WcT7vHah5;c?*VSI7aLE~oT{8&UNq4DF5REPJD~0!$2NO{P^z9jEMP&-;i9hBu>RE*#WobZ<)udqK3wunLG zc#}i_xz#f0{0GYx_ ztvBsZ{YW$Efe3y5$xwIB-d9-B9PVZqPeZ<`-$r4(}VDP3uj^K3rL^92x*rw&>2 zKOkUBTtqDJAq~9e$iQvld=uNo;2gsl?R?xDa{VA)mTggm!>B%vh^^Do1+Mh|(FOfx z0aI#!VgV)ABT572%e}EsPUj^fIDfdTMJ7ilbpGA8Hx=W2h?jd+{ub;1BdmLdz7u0V zB8|C0^L^*RO6i3!Sf@q!2h})e?sSXnr}cME#yT(Jsk52A-Xyr%<4=C!JhvM2v+8#Z zm(UjXI*K_r>3|t@bsg=-#Q$OMJ)@fJwzg40RFFpyQBbKCz(P@xUIhga0TmJHf)we} zg%FCU2uM+y5CRAS(v{v4Z1i43P3RCpLQ9AVgq#)K@4NT+jq{!H?)~eGbDlqrj3sw+ z-|L=hmTO*f&R@u<91L6p$UyiX8E85+)ifjDE^A~DLa3B6?SQ#>;cY<5&;HNYe$#*~H->TYu=z}{I%+mfKkK*S z=8tXB|aH_+ffjyyWG4QPp1^yPf^Yyp?;ZlG1Um-{R5s!s`MQ#}S|GECr zokhOKj2QZVwE$pO5L**w0TyEz5?m2)>c2D{ywB9V+T!tv%YkW1tHb_s9S!3PERx9z zecK04c?Foy>Oj_xqCD=oC4Nm+Z5)09>F>6FGh6HvI7=Gpzua7NH1uQB%)OXUpM$)@ z`bXW2mfsyuW`w23dEJcK-~CF;dN84QoOjWvz5jS}R;9BCJWKU>lP#ww#f9;Xex~}EKh`-Q|9GDBb@~a+|MC}I;#{-0 z-#%$sj~#fz9%QDB``#`l#2t z(#Ss8y_rw|JWjpzvzI%vg9Kg9Z?T(^F+y}YJQsp=vh~*)DfxN! zSn{@ie<|c3`Od4sZL+S+ZPLeZQx08&+*$iU&R@Sp>vf9H?RSb7J?8bSym4FIKDav5 zCMJBZPF0A6$LZ0^ujo^8E=*=KEcgi9jPMl;fJa1dx0USM;s~{61k@I4^A&y-qwV=s zk>kcQ*F7x7qeG&Zf3op8!yjv9K4Y@Ev2I@!3c6eVWE*!%o4o;+X0C%JF7dC3&PU0& z-rq`ljpPY7*^L5|&y=apqr~4b@Vh8wyDXCl;HJRrCnaT^2IHEd8sC=idLR5*Uxaxy z>XSS<>0jLAbn2pU$zSFraSt~_Pyr*HM*&oQ|O{GYvrAMA#iUetS$P3OZ{>;Fw9$K=q(IMqY>}zQY z+9s^((_M$$`*ZP*Alka+*Dy%uD$-4StirOwC9FSUh~huboCCi8*1xfIMEJpo5r$s^ zG;aqN-hC{+=ViOmQ+~radromH6@MP10B;j>Xh@W2g15<{XHP4l$yj z=RTKB#iK&tK%A9u@NBKj<3ODMjU@#0&w~(L`WbBi6wO@!a8Eou1T3-r>(YER{~wsZ z+4WYY@f4MW)1?P#+Lm{nYy+Z&Po?i}uY%6OoL4-Fhbf>t3W!Lb`<#7q^tt*Ca3C+# z1t`d~oCce9{rk;RtoJMScw}W2CWmPX9|1r3fBdtCJzuhB3g7R&E(|U>ef88n;XCU9 z5sxgj;W!-H3qJL}h>!^RgRG}jiS*{3k#p?7?KJ%9{HoM@DNyfuE8`ed`kUha?UhGE z)SDQ$hKy8DXVCzkG`R2WVLm3Mp7!b2g@rv_tP<*Jta>+@0H;R1=yO)8!c7(pAdVXA zU#>7e0Pn2mlRd|g&^zER?gU0yru?~!Oj)ts7BxDaOL} zv0qOf7S=CfvXu>0PXRyJ-oQKxWf>0H<6(11xSWlp-5eaEg=4LRGoSYZsde6yq9=TZ ziTP*b!B5vYm_px!BXD`Ip)&I^VQ`Njx8@EXer*yW&n5l&+sq#i3#-kH;PRD?+{4T& z9P)4d6Eej7=P}GPuDV z$@g1R8CLYrTwjaOyB0x6&FPqkF!ep5Pk_w!1>6IzPR;?D{b@S?{_tyHW4MaNa!%bj z&s@KX%uDm!z%sOIZQ#wuTMFVadzn~H^Gu~^|I9oZuD%aw)eE2RDV$}jjh(T%6RDZW z48HT&!+icD%v|hGC%G;^(TF_rJBZG+5fST^bFRv+P$u0T^{vO-BI-zUC@bSu(AnYw zc>)sNur}l6k06gcmI5=|;XbHBF*NG<1|_(BX-X>V zR;-+F5lU_s>*jpqmUh_)`DHP-wuzK8tsnf5}A zAw41n9-SIr6`A7ktWDkI&g>E2pCKKu+K0`IRgM>xoNlD=Dq%-)-XJ(Ml6!W5S_MHP&ROFLLd0cf$qEIdcmxXbLEG}q@lU}t)rpJ&Ia z>idqZ8xSf(Rm*p#!&m3uY6~Tduh!%UcLkyZP1WD8)`ht5*z(Xb-_5j?A;}hI))C3Wxhm_ZM*ef*{DGfb z8li^d(cFcblAZ*+?lgbI&SItU!K^O7$q)Np0Q?1NY1Yg1`A+5@hB-cve%!;LXpn^p zg`e#goPR)Rf?fS9RpcaVj|;R4HYix}wSE(m)Nz2!jS%jX0aHr$U-9uY z%3h{oQEiPv4QiF22m{A{G%L1I#`AJ>f*+O<57aKrlqk3#jXtj0ji^mP#*j_OIZJ_T$u9csO07-xUqVbS{Ljc7YqwLb>+;q9k zsY;yj#Fi^q&Fz@tGMk^5wS`ujONjIRY8W4~tLH$Mbidg82_}}v!=GNrdkn0M8%A8H z<$&f7a9{^(T}wu9YwrnFX11QW-lI!xM+c*&9I(}ss>W`$q};SkAFZSKh;t<`Hwb6J z+cF1l>-Efb%0>Mrl?eeVcROXG=7)Nw&%_xDcS3JN9}YEQ?&KR4K6pe?qY<;s`^#ZF zpXau}QqVv7N*YauEviczD8tTIY?t=9m%(1heWpLnzngtwT193ymQyIGLY-hnr1%n; zaIDJi=hz4CQ^dN8V5c#gJUMc*Wl*0fo)*x4!gE8%i1Sg9841=vS-@>z^D^XB zO7SA&v~^8h#1~IAMc_I_>6TDdr*Z8~;AR)941?URM~E+(NqYBMniCMKl4NN*;FNsdcj1)&6OFE- z6PpuZyM+2C{nFu#{mkO#zu45O*b=NyGgJ0`{~6J}=-|QA(8&7{-U=ifL!7^=)73S5Kiz>M{52A;`!`O(u$w<3EpY+4Lt40S6)NM%C1 zNv*}rZDXdx1x^W!`K?Q6Fe75axL7<7p8X*37MG%(k7tEGDRHl9l^HC$+ksFk*d!@L zsybK$>p#J)^Tcp2lKT*!+d|d(28313+GGFOE))nGWRq3W7B5v6I+nliFIjetZ)6n~ zJ|e3BB<2PPEAFB8r)sWK`AQ=!b$74^^*JRfX#YitJi{`iLamntWa*$a5(f^D+1BJD zeF9(d@QX(JGcdQSM52rAEXn73pVZ9W^`WL_k+MNq%&6^HS7SNc%JXSX-eUwqKQEw^ zh7%+{-?!S<(-DRV+$2gHv>FBs&YwQTH$b=uq!7iRu z@=vkUUwD%;u;I&}hO5)`yL20Jx?r&~?LrPaBh3xPIvBi`0jDKsEt5-s^`=to1+Rtf z1SLYzEuH~3>cvk{0Yu;UJ47EFc#rGifG%C6FYW^kZkcRp&{aBr_KQATS#x>N#Q1@{ zN_s`xz-+{>=UsYVYoK~1Wg*&jr$)yuWelc)+EmJUha*{|{$xaqBG&$q5s|HgLUTTB zsg{Oq)%5?s=IoBU^X4rG`TxA!jZ0Oko6V4?x0<(K)H9R~R2Rc03n>?4-w%~&om9a1A8$!x{K@+S#;VI z)RRVRLw0@i`}qOR{1Kyl+X_^D8zc_syUyz?7UJ`}p)FREJ&JmZ*r&5^@;&K$$u$|I z-~ge`)QADw*=Sscdm~NMp`v@zD?zdpG42c#D@U0eSuzS|3SHj!t~aKY)R{f#d)-GH zYLD~n=JV8l$_tsYBDWrr(=T>Aws7WArTUSvqqQ?X72tL4AHwhZ@W)Yx-HNZetIz^> z_rY!7#=plMvQ94yuv_D0Vkuw&4|exVbBFA>%OVf4_XlU|pra9mL-B(zeylszwj}-# zed*k1_eR~hFivANXCGtgY;bLf%J(3YLs!iZKF$S9gzt_G@9`|X+kh8*`t@2rX+n_j zq}M0Dnm?FJUH#*`+3T=!#TxhZp#VZJw$lld^o;`a<~N{Vey*fc*OK?klsZl%rgBrv zozA3k=eMn9U6eG#V#5DNOZcg$hql zi+GTzciLRiCuPPoO86ng^=<3=&v(o9vydF7zLRx6@NiATISeOSYS9-fdO+aWCnYKA zP)kVK06K?+O4)mW6XmVfyo^$5CndU^4Hp%UK-|E9#U;!S(xv5P@(W$~uDNo3U>T5s^p zyci?3ew5%+r5M#*ld7T%J@d&jV=2zxT^2>Xt^L(L^lZhMK{b?g?7eX=%JO~8=gYIN z%q9-tsG^oMk61L!4yg4qfaL*kSu|A6Q}@~~~1 z%NVf=9adNW-pnR7trt9%rRLqnFFT^{b=AHuUX8l67duJL>bxS>#$x^BE=WM9Ac=~1g>2_f$a!_ynb|%UaBT4Af zb{aG{V|1{?z%EW4gVX(aUnmL^?@XG-0Q(vzafi)ABeuY&u*!4gvXNRQ*3(jn0&u%> z&ZP4|QNL4vFaLoagWG$K2HO=)B?|Njrk#|XP+pXYl;o>Y`CxawucXiO_iJFpME!rn z)w)FNAi}e1cf}uvmn*BvhjU=$p(RQd82fDZ$~OLu|CULw;1mD5 zOxCBPEI59Re=E^Pp1r$@x|z}#vL$%Vndyu+r^ma8mYyB`bjQqeWvtesjIGZE>cD8^P&_OQE!ZzQ~W|1_ilE?3QFHXJqpmgoc>iAqjmqtg?C`mFp{IGR^pyB(>Q> zD_EhJvv@A z>jmct4V_jBT3pf%tG*?cd=2n+)LumO?UF{z*u=)2vI)w%fl**vrT73B&|!DZdt@y( zLG8ehE3e=pY44Fe3JdSKZQ1j*aYDg6cP+hxR>(D*k8TC4w}Kbg#Qq|ho4V(g`z7a| zNa|i*nq|Nbec&_W6?}^;hucTbgh}$jjqP*+}Oj^s6Hak)kH4um7m6J7JSht z1&BmBi)*qY>;1al0?GO2`4N)_!MQNP-GC~kt8Oz5g8pxmQgPAA=(3dHZIZxT1DltD zpXUN>qrr|e9WPo~RBfq3uIvy{tW0X8k%W&dsR-|$z7xWbJ4(9$eLvQhRc9JJi4rhs z5;|4Jxej%csJhy=#{0+P1%caavn75aZ=xN#oA1t~Prv)<{bE6F)BCUxD8r!-SbPEc z__?QCA*HH+#pQMTj?)yS5ujLM8bApiK`0ywbG;-*zN@RRx7r>%Uj9e*_0PUo2ym^L z*3v)l7}GXMo{;Q<(Qn93R)hvpns_%>ybRjFh_rd+K6$fO;5<*Uf%`y{o+rtyZK!S# z>fIqtxx2%K#ywEJQvKGU^)JTyy|nH>F<&mnD^3x%-(IYanEpuO1&Pq0-mlYHI-&ty zHz1bnGPl#c!v^HX9wN8hzNC#;YC3(1mMf4lj;SBkOF~&LSV2i5{_fu(_kw4mCym*i zHySv`W+S!&k6blZ*gEMr_@txAI0RdGn;80*gA$;pFclK^5m&1+G2b8mYrG08tS#tK zq@;p!etUrP=Y)42gs&h~?piIB6e3mtXB> zdH~fU`L1K12t*g`Bq=>24>A$!+mDI8<&H@3uw&k_pI5XOw zNKe#-_#{gqca)zRXFV|hx{z%`LG*M;$hh;ZT5~_!2ga9C+&Pxzwr=qrEG}HE5FtZ& zgWw=>Q!FF!tW@L1^Z4UsRs4ayAKb#36fGd${ps-MueKcgl7jhzY|$J_&xZ%LOY7*0 zfi#kFe=%e+v+3ieLie~br|JY;&arGDd*v}_hjlPrjExv-di%QYL*W$?fIfE2dz7D( zpOFS_#<g-H~h6ULIYpu)(!j5u_`HuT=ngr8Gc6@o#p_=gOAm=ME zj!g_id;ec6aK(OrI~eX;XJTTRsDixbkA&GP^jvWsY#rTlA9*C5@^ok}!5g4AcB5q( z_hzZ5P?g#f7!uJr_rs3^tzaOLlAI#IVSp0{lVxO{DM|hp3!qv1nRwQ7r zarbv(!fKF>d+2*&lKLuQxS`H62@!Jg=u_t*Q|;#HlyisD*b$xA;+ILJP(}p3AnvD3 zOBAFBt*O=qQJ2}9{)=cKnJmQ=++wfPF&|bCke49?sq>2erSAtP2Hz%DYVdk1wV(`F z^7GP8iDgt!eV+Wrdx!}6r)nv$_b?kV%k?0C=-mP({7h2kC6>a^Tia++af92IzB*|3 zvU~6DZjE`l*H$drxs@I~DL1Bo=)p-{J^?o6Y3Qv8Zj^$W;6y8BJ(FWBodV!PJIcm9 zV}kIu+i^X(Z?Q6Qt?}Ri8y(;Yo}2l(0F!&ycSh_NGFGs7tL&+DtR%s27?!jv*ABR6 zzIs$u23oemeD`O>B}|Xa%ynuyFGS^TyNoV%>(?8Q)xJB?a=)CoYMJ{%HLhRqwAUp2 z`%Alujw4UK$l3ual8RDZq^}-&o&%(Y7_9)yF+H9js?Km3RaDOvvjp>2^;xi*5zpJp zMl`v9TV1#`1nYYp=^#k)Z5Z)=k(7gn^X2t;a1+5WU97UWS{I7j2Z_JT$pqVXyZU@& zG`FR>ZfSY2+OLp_r?a+JFp(9nA>sz*O$eUZdiLi?pK(W$ncMsod*7p<*qmqK90y6o zhUi|>^B7P^ecFAH1_Rhu_i8|WmB9S#V8L0p;Lrg_o5o!g==M)mX4WE}Pel1?pZA}% ziv-G?9105`&$z5A+ZuVBO$Nq4*4UU>?t!0sLDuB!GCF?Q^D_fPy=S*t z*5TG)+B2WOn(Zq-Of53Jw@K0jz}KfSZtHXu)i>$wI~GRD75R}{Y~>b^HZ_m$M;$*q zYMN|q8uTMIacRog+Lj__?ug=@<+BOtL%u%!wOg!mzQmvRIrXmXTT+}Y;m5Mip*;f( zYfw3~$v$0(pC2UXU~00If@?kgWXJ1iBNcOu;;Ma$-#C9Mt5xZ}F{OFnq~MS?Pg>x< zJTyB$>e_O!rRVOq6Yo?bdVqR5HHG-D4dLsxJm7iAQd2+JqbP!aJ}hhRtwPoSid1s~ z#Z|4iXsVi0Ei;DR0RYPsef6>o8^okS^LlOD@fk1gI@B7{yVc+GO0ML2`_a+m$#b%JulN7!)pAgMnb+SG_UnOc(5;%*_zuqcUv z7wkig)m-0YuF-l4?1L&{^ z?9b>6NmQ5nx(5ex&&leui1a$2hN*HRX1mP{4)@O*4E)c^tC^Yb%J znARQ2>P;FI)*hoo5SMWtCbHqAUlE1Y7wWfOh2q^Z-nTB4D^rxFQWu(RRU@PZkpoRsoOu6O!=+<#$IY_f-{hN$6}4tVa;_i33Ioqr-Y@Y%|G z`25$soF3fvLV-(6DZaFioi~mH@cwG3iL{#!M7AH5=DhbJYKnH6(v+MYfBA7C`tXO2 zSJ5X>mQaM3mm_zz^+-ADWv%S0zLe;R8^L#^hyCMUsg4Yv!gSgBF)=&=g?$%f^lb&# zz_^@uPK$pvG{iP+6UI}bF2(##KK+cS^TD-Oc30}a$9yLGtpX_ia%^}2h1It`&4Z7% z)22(i&3Evp$=3`P+^+vlXSGd8HFK#0xCTT&IUu9O!!70w$+0vz>eZDsZms^+H}o_4 za?cb0T>jv|AzDvHu%+k#+|tY{aNVDrz8KmaOEOTNPPkvHH`^SWu$U3ttX)znLg+&z zE8(+r>)OG}0xUxzi!4&P*rpX-_0$2eu^e^9bFjCwX=0sdEa`wg0BU~X-bAp@9sQF$ zIP)Y=*HzngOEvhxxFIyc@7lP{NoLmjZ13F+kMfB*4He8IL*2(+tCAaOcy)!jxF3^? zsR381_U_Oh)f{(~jeXc>0PrRZ^xqLD4oxyUg!*n=*%TkLR$PrvV5B7(0C`&9@>%z! zZ8=P(Ha~_$-)?M-RNd^ot~@COb$;N3QbG#YvoNvmbBW?kF#^*&`uZN`{xu8q@9We0 z-YdOLSo7J}Loaio)a62{3Aujk_!ZLTOOk zrI4^ax{dLPcZ-Xbop`9a^CXmrg?y1yAfa@>|C3vmXTQ5XJ@QyTZn$`?_Hi9&ofK9x z`lQUnfk)ef0rRRcC%(mp#aP)igU(j<`H=E5SU~(z$FxIhHjrhCELuMGtmu_1VzsWkc z%bYtn!Ns9G!>~wO@Scte--+-zz4W=#VISW5T6nl%&{rClb+6}bkSlNjz|c(RFn;cR zc43Jb!86DfVyM3Q86;QE>%j<1ZqHU~xHi5=_2l}VF4($0;;ZKr{tHAzp^TtmWYyt| z$LUVe^$#fV(3`iff7M{X{)x_8o#PYff|YVj-4cnzbT7vS zx((5^+&=*H`5kR*5`$$uyPepIzJd)#gxf0W#T#73sy-P3Gc9cUE9Gh;p6VtAN-fH7 zC#lA%XOw(%U@73eG#b;3C?uq4Q@KFI?Y;7}Wc$@Y2`NavdN8yoWFiWU!f!Xp7T6D$^3x*`Y|yM`6*$*Ec@}XjYDrz|5~F7 zF|lE5EI4iJC+UiRf3!Z|+)}nKej=XRb==!q zERQ@VC~FUbi$CzUXMLKV@3PTC$~d{1T}Lb5qQ2_(@nHCN_8toZNb-Q?0*EIwy+dzB zao3)`8q`@c&EvO)E2V3{vbcN+mBq-m{srXJ=2!3K{u&SNYKI%pN{`-5)c%0z%pcqa zsuxh^N~)C)@8csr<5LGY7%>Ag1DF5&+ZP?dMQD_mFo(W>9g@QU%nCg+vT|w!rpu`CPOOu(5C=C`$wm`f1(llYusDUtrwwgg4agurqC}}Pfj>Fg^fMwb{N3)b1VSyV$Hn{gcC8Z< zY9q*;cow8bk1ju*tQ@e*j$S^naP7Vyo@>a@Lh00TRhQe##XRw%25}5}ORwf51(qV7 z&m#JB1#=*R`&gk?U#pHl5nVG=hAdSH-w}nrGOAUq$9y=1Uc?u1<7jC zhgkV@Aa8OdXc!F#soat@nGb5aD52SZV+i?pIP^ zOYOM|)ELl*=jKwvhxJB$uP^Fb%mW&aUm+z%$^B4+yY;5R*Dq)8z(d+(Z}@oF4CwK- zW-=MM^5|V3Sbf%e$$RabBgo8e|4>Az&4~0udQ^PiHW`KcUBew&M=pSeUuxVmHh6!* z8THT%9e_KOa>>JmXO=_%z^NZ;F&f_Q0Xw#q7E{n2;N1}C9D5FlJ9P4Dg2&?$9ooa{ zIvoF`9LR$_l(^I8VY3MLeOt2VqokzD>Tnzqr$Lf)Wpj);JLQG_W6r6g++ePv zZl-ahv#5Teo~+{(+V+I}r`XjY#Y229qK{k5@s{#-$nA+c&^I>X5PE*>(dup(7Nyh9n|Z-VsOL+y~Kn&m%q$vfp+G z*o@UjZE^uRlSgvrVZc^hFKm8SWp3&R07w?=GlT+uF{LQFzH?VS1LRdAv+>^Gf#!fs zZ7ZK3H`G>HAZ6J&oo+{O)Tw4KK<0t4EV&l@O>*VEkcdrIv zRk3T++Uy?R#QvUI|qL_^O+>fNom%o^B7teQU3X)%ba5x^uW- zi;!PgeJWZ`p>bS+u+`xCe0P~Pu&|96y4nG+nolz8r~>)JpkdTyDHCc~!_Gz&qm*F5 zJp2Ufi1?#2fxE5JoW-z9cvcl84lcOhRAli3`gk9rXFRaVYXy|%SDrr@!EFh%Yl^f= z+qyYkBs3%ARPpl736;L+y}3;65!@BH?x)ZJ8KF2~w0fwsIR0 z(cZj9Q*EjKg-Ka$2K!|cTCFl)VQ3cNH%!TEqtp($13#c zm7r3nTI~2C73et`dzY1l?@!-tUeFW}HK;We^e28J+U!+Z)YZbG{Nh%saOs4ARQ^H) z7I(2X?3ElQKo3br7Vy$CDStfFTd%o63J3e6z##(yuAaN*I98Fu;0xu?Q8L;4UAS^V z7@Mn}@1X(F>i;P#K*SJ$+0h^%=#msFPRg#{m9HKp6-*r{Ap|0{oj z%MiL5OpH$tG64~8Uow>@epUavkILKC)ErY`9`&#H7%Ka}5a4#lBUDBm6eHc+px|ZU zhz<;P?3?i>OQhC){SmvPn|m7}o1^^n>!m)ykqT`Yy-d!NF{;b*ec{@QPn72E7xf}MaUO3SKMPAqd;YmyQ{@{wcP76oM)l7 zpri-*>PC!&x{g@-GCFVY-Bm0e5Kw$JkDio6U(=WW@Y1DXx|yJr4kesF5j?b1aAxI(kn5#u(uG$|-e?%NZS@=?h?Y^`^rA zf#+4f;|;FAnf`2s%YS|-d4j+Aaf~V&`@%=S4-MP};enF8@_W2t^izk0fpK}}4)3>- zk;ARG2zhKMSUpAC)|SFbGX-8QUD_?U>n0hdw9!I8SWD==q3de!!ru~x%n0Q8ljNW7 z&OCxD`N}o=E=Ug(;B!t&gCf1M#Cl-;LrY z`yxH=aWWZfY@k6^c%Rf+_L=nH2VflhXRi(?=csO^5a_6OBJP zBxR_kUpyg#IF*bHEa~w|JAkgWj8AnI0FGF_dTk3^u^M7s+NJf}=t69|VA%3~_v1dR z9_f(1%1=}Wz!9rvg#S9W_%UBkh(&AVOTY;Z_H26H0aaUwDpUC|E>UR_@0^hWP=+Aj zZe$IvI-K%2-HwYeM~Mfq0VxqS%pBZ)KNdE&_TjMzhW6KFlJEXYF2B?<&^xQ)3Z)Gj zS#JOb&MSFh9Y7&^D(W2O!me3pXFRIWVJu-nNGG$$^#?zTC)@ic+DA7!&$uYrOadUI zBENjXGOyAwuF4^U5M@-W{2tuEH)iKLy}LKldR2zfKagsl>|Hc*LQZ2H(&N~;TkIu{ zEYvzZu)eMS)idLSf-#jO(6Bez~15`Zs1mHFhE;iV{4->);l$o}sz=QOk7*BpKx zo4uzNz&>`%UNu+JaLFfJ(##2>fs!Ek7ngJ#j{eP0)%FrSW^yY>wD)4zmTW1a8CM#E zxE9Ps10n7{bjW&AzOg$LJ6YK1f*6uTp7qnyM_DF`8X!b3*3t$IyW`>w6ua=zI##+# zqh1ZhRpja*8nwR>b9mIHTq_@4Tq%|(q8s#7(DYE-_~Urll>mk9YQRAc#08NjtFJh9 zUZTv!$2N2^_%#QQ_VTGAqy&V=eiV`mwJeOR`f^nI>D%A7J(KR=>O8LM{pPA~(C2u{ zK~fr@hrPGzVxR)>FeYw{RjOGeNm&ts^xef5%rG#&_)Yuh`ZOVyOMm#({9^sF6*&J% zJJ6CrYKS|hdmudn4RJ?~$MjYm0ff*5QE>29MqC^e8lC+XT@S;kLE?p|V^)f#7Y64j zfEe9f{rGN`Si^H&N7OUi27?O33up|tC_y|&#IhRFTYb9jA^y?@KbB9vY(&^8QL!ym zAt#ZB3j=0z^ar-zjMeWn?m{z4xevqh*vPXjMxp83LR5k3alojV7Xg$sa7=B4VIFFc2jLX?VIZ+ZHp{Y;s~?D#_G<2q+;?mp-~CyfmyuNV+7rI8HD5_i+9dh zY?mu_gdPx_2}Vxsuo7PKrYAa=2xR1#EmekB+nmRc3{5K%c*JAeB?vN(Xx~% zMl^3d#tmwd(Ogd2EHtX6{Xo$AD z)VPE;N|hPEJN&NF?%RRYV+gn3p`$GORS@VhnEY3Qca`+-VI#B79gIFv?tos9-5+LY z_i%8($#Eu@H>~$H($>bbg#v#ecYe*rYVnVrV@_m*Jx`=!#$C#bpBR)-kQpA;Vh3GO z_dQiSU08pf2mgDNN_(TE{}Zb@3gU#R$^qTlSd(n6Lyp5PVkV^Vf(G)z9(kI$!fs9c zd~s~+DP9# z(_j-0USHHCPsf=%ZNPrL*llez84#HN5v$q?dn33o7PcB7k^$v6t7X0KnYLDv4-~Wn zy3;~Z?Cf^RO`s1of}iBkjmRqEq&m%S07p3pwS=`so*#gp{!^Gya)^!S%)FIrQeCfd zLv?*FJG8OHl6(8BEHJOiEWkK^gErL>@M~OQW#}h&1+~9fo(@q@WBwT-?{l6nv>jAU z-Pcm_JPP&#bloycF5^1q9?IYfl}cLzFc%vT)Uu&^d`plC%#Q$-BQ6z3->5f%60Hll z7ZeUN*2d1rnjX~3WYhv4Gfd=X1*6$O|j(R8fpOUh5(45EGA^YF0=mO(EoR@{rziSoq*R-ToS|qY|?*l z1D~}4I~B5=dIr$j{)6j_c!WdI%gc*XT>rtd?_q)f5=3=giNd}A;HLp)`R_;n`>p@G zQv0{S|9#f~ZC&-Zn*0B)hQC8X71yVKkSI6#Pi)j@qeqmD3|AFzat-KXeV+!xqvsCg z|4D=;uFg!8b;HxwesG35e7kl);HW&JH^M-9{e^p9t{$vz)>&rEa~uQp66ZS=iY`02 z`1C(8$u~oOgK2yUhrr!_@7DSczT&g#?{b&uK&G32f5qQ>@!wwM-*WL!tMm6>{J(xL zK0OI#@H+wLN)%UQXvWCmCP`Y{>uc%wb}H_AD5ED!u0g%Y@?icf0sWh*q+fR-qCU0K zfYIm!v=Nq-fih}Hqvb4hqc;I@>3f(niQ$S!OfcO-5TyfAR->^IJH(ZRf}|lkQ8{ z;OA-kD^I>Idum4uUJ!!rSl4KDJi-@>-A>e#bDbh!d(@=j%atlFgdw(8XY4`Knb5tj zazq~lOu8Z@GjHGiuU-HoSx-Qz(-{!$`uM?X1y@uaBMw?hnAEIs`-5VVc~BDj4z#?; zQTBu~cz*K5vxpnRAO`n76i)VvAZFf>^mgX zF;DAQuQM0x(+(;wEOE~P^5VVviF$+BvPiYxjcG0XEd1yoVJVIE@IVcIS^JMSwfzg~ zX110YHD(pLz54EibSYWy@mUJ9Tvwfi)8MCR!qPyG@+of762d8Y3S<@Y1JGWQB&tT* z;lF5Pj85`q_m=hwS-$nIN@I@4}iiT&VpuNpbH0 z{DPD*l#SYx@BZOiaCn`W(qcgrZ}1sja)WRoa=>`Rsj%2?B(4wiu{DZa#BMh9lp?9` z-W-?t$rnBEUS9>D0|gEEq++!ZC;w))FD_!(m-?_P7fE4xLV$A#uP@TiHJ~DKyOnC& z{FIhy?HpgyJKg#=kX(QFySK>918CLt#FO z_4d?aWA1<7o;6TDjnc0xI2-Au=#!|)snC$=Ln=NsS?9+EEmU9l+#y-7gF%PgQ~TVO zmZ7?fb;xKHQK`={h$Kq05l>V1M!5mzwkKTLFL%OYv0?C;Lf}J;|9IQcJX)q&d!O|# zWVJOU6wzUg8rA+EpU%UeFCqghXID1LvJfY*P#-u{B!XTem!IZI)qN}s+@&~#ZoYqi z`}b4xnsF}~=|BofP0DkXDT9x*5FyFqk2IMPyRFxPSEde`8Zc-`W)7h5N72gi*`^FU zkr^3;IMi@suDgDrt;?${_^1qa3|qFYMKbJ=FeOG$fYODHw(qfUTf*MCwI4BSA}Eaf z>bcit#!@%@yGQoJ@a8q(6-)Kg_0hqo;e}1dGh>4T8n%m9eGRN-RGW3 zbG*#+CQwK9-{$%M`O`Vg->EG*&#`8>dPe&Fd<2I(NTlXl#%Dlw9cw*z&YfxL($eJ? z4n@k#!MK;A;%}k*WzY8dtqvRRuF`i?zh_+Xc%w|~jL-L=-2px8Qj{HE4Hg)6z;~N? zGjw93>ZwU>fzsb*+uX&Cw`Jqu7WNKuu(k!h(x|SF zoeGq+PLsY(8Pv>8c&W z|F&6A=rbADK6>oJEtss%bSHy@K3p6uGIM+}3XP!Z_GLA7>NNsd)f+yw@3rIRjnh=v z`~uAR=JfZxw)1&&NkV@3gdW+T)9wgbK&14PF=D|K%nCGJpg_ngZ{KP?S&i%{eFuAE zl5jEIqV2Cx4$xRE&0=(PYRK3q5m$&g+Z34?_k16FJ2>v;<~n-foKLz# zF?~^u^6~Budr!Cd3kwvd3LhMgKXE<0E9HaCnrVTz>s`&}XJgeam24j>H+TSZr0_Y1AV2rn)2Mp43z_#FZ1;G)Fr~JIkC#GD z*mhge7Bdd$kGa&AIp`Fd`s94{5WGck8IK(@8?T*l1IuH4 zP4B6%LJK~UW>t40`Jh6V#j&T}u5)X(ZwoazG!A`b`XLe=y0d^wwhTb8z!005#2pHI zD_Fi>1|PZPj=nv*(}miy)`L|fTxh;MT^G2s9=_2!X5`t?C6i-1wA@TeObQFH^j2&hZzY+w zHD0>0_>KFqg7fhGQR-y%f~rGCl?z2{p%dBs;XA41|kV>}jeW{?O;I(jNXR9Md zU_5%;)-f`}0I!*W-n&xW-;<^989Z5u>NsX>E@2maxete~-8^7Y6=)r~`nd60LgK@{ zL~4rbNM^*63*m;ol7;6<6mcISPCh6-}I$Z%PG_$W2>Y^t7pyVTv9}~CPO)IT6aF8y=tRY{|Hpirk+eP;jR#t$d^VTjB72rt64-c`f zSiL{sRNt-=%wKKoq_YH@S%i#95I9T-zjCsr+lGA;;aqLE;6v>OyBD8JWQH^Q6 zmmRc%p=;lA&sg?Flw59p|F8qtddUuB@hZ!)U^vNPsK9Px-q`A9*U9P*qh|8Mwj(+7 zYtx!poOKeW$~{5HXZm$Z;}pp#Iiz0{U*n~>nn62`$$?v=Vr4e-N4AgGGqv1mKowww zHW$PzG1#tZ%Xr5Dv9VHpR*b8Tlp z5A@4nwOd-Kc4MpHQ)jdf@f<2pO6KVrKYnU%yi-p*R!~Nx?P5+4LbO}cwzYRx>kie+ z3--zZ;tlC#?dFY@Z>G$wHpbopJhs!!U%F{0?+u68SGkPBVO^x)H{NUgcI+R0@c|n` zXFe8pXlu3l1DgLr-KG$f)p{we@7)<~{Y86-1qN5Um~mxq>Q?h{V;iY)u0{?Ggi%C>z`qv)4%r=VGmS1o%*%yg@+E4_o3{@I)Yt&z7Ds2>NWq# z&-m?P&$%@&g~c@P4^JkyV%OI+CaL7*f?9L4;<}0_XxSTlDmTN{E$S-cv$g40dOU&z zI}hF61$^J9#C`AY$f`IhiK}G3=p3o7IUP1{q zB-|Nw?|skt_P*zR&)(nf{`3B~OyXj$vBn(Z8SR-;FP*$YhBu@}vSBJ#jY~IQa%OL} zHbmyUY6=gz-cwXDd>W>-_l-f-;W`0hVy{VTH+Dj-Oax~#ZqEAC8*RZDIPmRD(A{M<=`LdWjaZio(kb`Y=Sc9h>KvzBBD!hI#45Ml z2(<>j6!+q81PK!f4<3PI!|pVNhMZfj%_!^hNqI{&gVoQ~3lHHA9D!*eG8+*&1kd94 zxoM5L-UA7~8*`SBE(@1!@2^Hk^M|XfS%yn+0^zGqjwfRkEXbRy*a6GfDC6~vEma>^ zDrR0WI$E_|Su|Z@%dm;56rQc-)d3U+gIerN=Jh)CrmR@404p zER8|FUx3}a8x&qO>#M*btePK)2HQw`dBi8N`dyyGc8NPyHdQJ~F;+*c?i!r^(oo;2 zr4aPJWPUA%^@95oH&Jhm^G#rzkioT{w$22~j%d!A5!_Kz&4feSZdQh~%VvZ##cp*W zc@CMrD^a9W#tRiJu)EmWP9pqsT6d8}gCcW+XXECe6w72l zHQQ-exBeK+I@pve=%Gn0jm#@Ee}f#o~-UJixmLpO^T#K$) zo!nL9gMO(=uJC2nJcjz zKPu-mbXF}yD1Uf&fkil0#Xeza)Zua6uv~HVNWrC>mFv&bLW~zVZy0KZvst58xNPn` zOGsoam<=+aXVZYJ+(XUg%oq#hNR(|V?xRt1YR09G$@WX3(=V8@3bSt~+{S!_{B6{l ziZ8u0wk3LPMtc5tF!rzUE!}$xN;nn1Zlu!u!0hOyPPs8t@m^SBoXlW*9 znc2)@R)-5C#vZ|o3`^BU;$II(4zZKWj4JM>E1Tq@H%BY%I0;d_L7G>mT5m66hs#}} zwZfT}(cE6AFV*Zn3*JpEj#gRyVQf?ik4N~Gd@Kl#R@s}Sb{8B6Fz9r#asDyHg%n&+ z(2vji)dP^Nt?{kaT`Wlm-ocWHd@&ooh>o922BZ^4g>Hq2nqW~DR_yo1&OmB{O#=aU zTFTtzf~`cJi1&!Ucads^qH4a>a%Wdm$gU`&U+*EpOQY-)^tul9@4v%GV&*`)` zxw~$b`4J*Kx3%{bsAHEYYSHk#Wkb4~LC?pX2!`gqOSh%EkbqQHOX>96q*}Q53shtK zSB%@bu1uOum={LJ{U9DRahm~{s&V+Fh;{oW!uLx%ULjARy2#zNb1#RoM_+|-*Boh8 z%UmNWRzB|KT?7@l;t)Vm&wOaF9(OC~>kyXPYdK0-tomwRUDcanlj$lfB|Q6+%Dxg0 z4!M%FI7SRhbZb3VHK7E2EYx3i z(OgQ6fHm_CT4l|(9M{^NUN+hq-Oz~aHcpjV2ecKj)8b6C@`~JN2G^Es*I9V*19wk( zv!f}%xLC%uB%7@pxQ$4N|9DN4Khd*%C%(S%G;*PsN%TvOR43F?N#05n@VN!otn?JG zO-5FSL+dloZH<@>FW37UzLCy`nNE(Ue)2Uti08I{o|WhDVDOCvGS-#5io2`Z%F?Wa zYc5DYp`n^|zR^hr7yj_9)xb)~k9FPTL6?kV!rLIN;d~hS4PSmO=>u`7RuVC9dd?XC zMcng#u48Xr79r{cbllxrH0w@zVJ&pD+;PwmV`5f65uC6Tzm^;1Q7xsKJjV8pG2o=M z{R=S#t_^lf(VL?E`R%!UrMqDS-l!e-*uiAAT9=KtLHo74FL#(i;U>j13p)z${O?R+ zd3uWE>)E}RhR%(`)ge5#wXwxX3$nHP+(^sZy}fn)RK4BNN&g3q<8|XU8*iqjl~c#2 zIu(ZDitFp+1*^nO2eXz?hkn*YlxNci9cKDfsz06;qdNC1tdh&SLkahm?_=$A?k_@( zcv#UB;4ETq_{Hn+iA3W&h9V{ow2d+a*ea*!P@+QgW+wCTMW_7wLOt8|M8~>3(;X%L zX64qgt(FY#mdtLoMQl}Sv7oqd!DogF`+Kz-*}<`)jb?Y2FJ#wb{wl* z;3)Uw^a6_1pTJHV_8i-bUu)erFB$GWxdhvCW*i!ZNIjb3*Q76lCOdv*t?;0o71Dnr zAfWTAX#6wr`lICs`}QB@#{ER84{nxqcCVB`M6OriSyL~K(Gt93of<{@(O+5b^~X1{ z%OO6FAuLU!b7Yi;^O20?5MoopUf!n1@tnaA4_^~wYCLFrkJS;c9D$4@IIGkenotUx z1(Dsa+v&O%i0SX)OK1)5g4w>e)%U*{so~ikVJ?)oXBn1Oc`QcZS6cZU?ywct=NB^W zxV%2{<1J;wB9=eGSBqQkfp``$o?CNePA2L4<7u)RwPLmS!*l)bl8I%@wALdNUMS`2 zo3XOf)YKU5ClMhYwygE)_nr*wD{W^;yWAKbLiE%x%O+C7b5(TNWMbW>S`!%SM9j)m z!PG)l=vE(T!Sk+8h@PlM!v?14;eYfenjaH#W<0(IW`mlV{(rZaRn}=p>-^GmEqpvwT zaY(1-`)%~IVA`yaT9JuoVW``mM)DX&0`u^kRv=FUp;E1{Eg zFPzQbZLjZ`Te-N?&i0yE&G$Q5Lq_h&TyWZ@S^vbkP)g!d@%r%U+)d0#+LrA*LC;0F z|C`l?BgM8NC5(}q0e7oqQEB%53+)7-$(dxf`V*2(5J=2P6cM8JJaPJMx9yYiu?C~g z{;UvJMg{KsZrF=N4p}LP8ryDpj2Ozd6$J3b=2r;}9aH5k~8BU0aTFS$5$Q{>64@0fXy3DeKZm3p=4h^x3f z)`tz}=brRtQp%;=6$mpNu0NepEL-k4dKb@oWGnR&5oB+M5^5Q`9O%llixqC*ysM8l zUR@rkoXW2!J)2)XdcB&v;6e4*<0+AeFLqt*dduwkXm9RrvYurzfB1BCiC2x##sc+s zuJZ~R_T1b&*V{PUWGqZgd$b%`s2bGnxiIdqq%a5l=~>o)yjt!J6R*0pi`RpzT96(Y zt*r@*4`hhNm7A_jwYk(`9>A*^M8gaAaGRv)+=nMqpvChonY#5m(+)*LbU6$8Z1SbiNM*Z=M(COkK=WTC86YqdJi^(C)D}>B-{0Xd#s?S*E?QkP- zJ|ZhD@!V8v5-`&-Ws%-0*T0o?$(H2a4?|nm-@nvTT0xCRe_PKpQ=IDe6xZ-&uiU^4 zr)Nr-)QM$1(RqhjJZh>yjgK|;i(mcrnS$^tBgx;?Fvrxf-Yd`J{`l>@T(8>NEUovB3V{*R39oJ*%7k!(Q2_BteDC?%@OX%_v zENbgIu=MDF`OT%_v^&XZP^JR*3x+=fy4>F8u^*lR{`(}A@C!%-U$2I9IkQK*Z=)5* z<3tJ98n6Yq*KZs?b?wcB3H1V-X?aXIU6&%Rw{KBQ^7A_2NrOAMl2QW|N9wHS{Ct*o zGIG_H>O0<)y<{sNv#8IziF5WPS_?1B8XWF2!MzK9-FdNduRYr){p!qVMrM@d5+G<` z(P$qVd*=(S+$$1ZBJLXX4Q`M7&(QlcWyUY3R3X0A3j-X!;r=#R5%tNG`=P7Y?#H%U zR*jyya~QX#rL9oX;gHhV{6n*zX(jU9r+Y&5*`%MD<3?r{a@KZt%3}KL!;2%L`zuN? zr=5`PncCC&jI&jcxVPAQ9j)fohGv!ZUdp33V(*%cz6K09U2r=RO3I<|grq<3Cu96b z1iOkvBT;v7g;edfa&IlfHOESS6RW&Xz3x5K zxm=so&;4%mSsBLSR-^?#Zdd@xn(~^Hs@h|PIuFp+YXH%Kd<*H(GcdQuiKq(r#%sDx&AeMX!|A*`xYo-@Y@ z1NEjGS)d>SWPE-}Q7|H?pK%6C+{<(U8M-B-Dii^=z{;88-FXG|1~om1dlTncCC!d~Fg?1nx$YhI&XR8Tp(io7!d{K2U2kCj8`Fh_{} z;o|i})}%*|HWt{>TOOE48~H?i+m5995ftyjP?MqPkTyA5+v+Xw&X$c>3Nakns(^@0 z9UT}JB2`QJe7{BrQBBu|ZZ;5I`o21?!LH^((~xkBSO z&0!xr(R&=L@3U&i>`FEr4Z`jGJ;ze=0cK{EzqhU3&}`V-LXBq}<{d&c1|1}$Fb3UC zEbBv$gR@~Q!n`*#+O@s>MA)l59E8D<2Ua2>&x#SSZ6oTz(&cQcdM*3n>rnXI$)0R2 z6~e$^qxg{=DXyX}0c72tq+L(@t)O+bP)~ID($TI+wu_fqlRe9J%-NK3$5$Zk%0nt> z9v5ROyfaAA`=jYDYL%&6`a&of z-VVXkrZOhas-3L@vsQX@L$$SpsLcFPnd!HIwKl+!YAKg#te24s1|0~Hri43$_ouA; z+BQAMrNeOO?@2_ng*TY;`#L_8vC_L+oFY5nZ(@5=8D%%PyL9|XQyH%qw99I(TMQ(9 zEPECg$s~`gU-Z3F3VGAv%Sy7flyF2o-rOP2v@)D^7CSTwU)s21Go6zL%`EgT_!B-h z@c1tun;`p+8oxaFwb$*2S#`M-)$8iTn|jJJ`69*zJq$V~Qr8`#^r$_n{A}90lCIWX`3h>Hd7kM zn3#z067Wri@Dy4#c}Xed#6bNwo`76}rd>}(IwD+3b$sj7rN(>;Q%9V`;D@)1a?64D z!`2}n!Dh~V>;|oaYe;+U((YCxyYmZ7uEIcq!I3VLt#ZUlz3^1$2cCDDdL?q5@rq{V zW?Mx>c+cZ_gAcdt);;Z|x~hXp;g3i~;QSkeNP?`>t9&`6ow2xiMeFu~keX>iW&TqA zZLz43gNby73xI5wUba(?(Q7Z-AbDhKt}!CHb>#&$CrmK6ia-EfKOO|OFbkoSGD z`lX!?e@>@#)5@nYF(yDH8v|6%@ZmE&YEIevaf95bN!X9_5)hkg}M=o5e?q_4Ca=rH!s+<0}0 zer`Uz7PB^;kl#|ms(r~~6k^+Vrn?Iz#P)Shf1)?t?ChD-gosaYAXHd_#2z z1Lv#v9Sa(Zky9F=`ega_SzF0D_oZ^b{UC3fy~k-JBSpdI$ztg`$q!sT{+H{(^hFq-SCc5uJe1A>^a?YCqDg8A}aj2&m?}d_O9#McAOpew(Dc+iDYQZdWV4 z9Pxr#>ghth%-z7j4$T1K+e?f?ASzi7G5Aj3h0VK-r9%iIP62<(w*d9+w{HZ9Xy@H# z``q%16Py}96uM5l>7H*i$zYXZGDvhg_@2nn6?xiOY4*m%mmn?$H9s6}bt|KaIz|EI z>Dw22=mx>X<(kFR-uk}2LJ98tlOrfwZVBWkk#!3ehoE&X)BT}+Ys_L6CNB;U|G-BE z&b{o|)@LCqsT6tLem0|kr2nYwUbAj-0x_F@>D|x`?$#`5e&Gn>j%am+7A8-c>w&Qq z&`u5HoKo?`ch?q<2Cm|S0n`JMAG+n{7}vHd^pg}lTRF~|=R773v5NH%E_eppIdtFi2aqg+ zbjD~Nf)a(oI?7}E9*vL-VbS5=9MAEqI*2YmD0aRR27+~V3p@wLqJ;{NhH}fp@#5X=7Q(hfNBjq=m&|U&Fl{ z{6eR<#s@bGi)XGydoDl=p$pH@4)y^(a9{NyMtDkEOvJ`ld@c67S zI;9koZs^1&;CoL)znWN9o#y)P)TW|e+3Imt3AZTXkXgeBqMsT-=6ihwJ}(u$xz?2s zE2^-5?{VB_f~Jux#P6-ldxx#f)JvW-Zl4aYT-~ozThv8UAT;|<6CxsB8%doSVho(4 zzVP}y$^mCy@-Pn^Kv%r!;Goe|Q?)A0cCr)Flu19Bp0N7yRW+pa{+OY&g#U|=#-MlY zJU9C|qEPI}Og)~8~9++&9VH|qBx=c6;i!{T;KU@hE=@MoOP7>aiXb93XnP=U%Yir!H} zW~+n5c+JorW3VsnjB2dwHhSp7reh9GzSjl~HA_wadL=3Ow9_&sUUERmY{$crE;MYP z6gGV6i>n-GNHTW$!O%tSq?zk03@J77u*H8;BWTco$;Z z4-fc@bF$Mno9M(#!rn-|H(|Uq>jk0=<%P{^T&+N~U}WjoXHtT30Eyfyv^@IKb_mO} z0Y>8&>+W||dFPW1vMg-~#1UT4dTKm45w@(MXZM?0f>%d7w7Ebb=hF@u3^6{N^#Y;h zzF5VT-3?AhPG_YrYsDUmBSZeV!pjtO)v{tBr|8$E8aYZJaM?NFd2`!-XcwnhKQ-92 z+r9X3ey*tISLhDer}Dr#Gmdqb$-*lcKMUq?jSE55uh1a=-+>d(F2PM&y+Bvwa0$2 z+WVk}SV$+{!OaCn;9@76B8P`_UVAwy>TE1Hmzc9*Uet4?#O&^zyOEma6An3hz1|`> z6K7NZ)Zy#Wt#Xob%jI48Rr`$*cpJ!+O3fRi7dYwx0 zMu#;XmoITXf1Ti5a~sq^U0eoB?A{&agnR@d21x7_N_4EY1lOfBe{=ypGR4-jG$h1_ zuXGv5*#S_9hT#vVC@KBImV%ue4O~)TB%MK`B476h7!QJs7Th^IX6eU!c839%cQbpt!_HD8CPnIYQe7@MX~Wcx zM8Gy^u444fKy4CWp9pzf%tmc%V7;+8|JENzNb#Q72YQ0L5GKlYop zap65SXY`ZOShsV75~ZRS%I2}7Jb)9Y&}$gEF%f!G@8Va6*d@>zBW+rpKD|*23}bT$XtY1o@yvG&MjmEFyQWOry=e`w0(Ldq*yg zYy_PUH@~bWZXo@AJ(U*ZKdL$|I#kwSpwGJwk%V-)_>Wp7#z zab9#(qPY2S?7i@=&M7uAtZI2FfZpCJkKgjnkdKn^A1}W{Zj(BE>a4YAKWX7;*Fxiy zBZjTWHB!@9KBEP>AQn93Dy6iP!`|&Lac{S;l9_S(e!*RbHb9Xr7qderV=O36ewe>AoA=y&PP+gbC#tiDQZW_w(W^m;;AZFEk=$wm%*K`Q{G2Cv;4sSDyo~c$9H2{gw3)f1|*g|y{0wk zZzdCWKruJQ@~d9^Wo}W`{H1wOJ=`Fd|8 z#yL~4?!ia71lX+g?7E?DndU@6U+)?as0Y;{4*o=O^|8JhX3`^m^I1L}X5CcyWhh(0 z*d$0Cv)0o|H2C=xQPgtv)tYcueqYPZ?R`h;@3fB1a|&n0@F(Z2fvpOO7DB(e{N%9K zQn|B-|MUhxYe@9PnbwpL&4KzyZ=2LjQi+d(xiT0JZ9HiI;euk4lhGcpHf5 zaZIoOegU~GNkGWDkPNukUg5JDG0&s>-Ic{aXOFTk7y;+p@&Bj(h zt7lV>If*K~11m0#5zv`wbMI7fs=c;~qsNYmcrHkvKp}=}XOFfmgg+=kWBm5ES3^h9 ztVN@g?V;5B2-kRt=!dar<>(H!y8VybMYbJ>y9*pkKlFFJ^VHm&7M%wq`o+l`3$ib- zzDB>^JFwOkNF>+{Z;iJc2b{JF$5+?VBL|$)PNZPvFoTQv)tkZAQs@N;y$z(u>vhdu z>ycea-L}@L$f08GdQzi8yo9@3(Yog}f_^i}+x#Tq6Wsz2%7zfFuOe+L%q`4@d0sXf zbP)Jw?NfRX%XU4|b6051*4S`Q#+KS#6wlUf|AvboK_R^u9pPA(rAld)aA zuroW!xQNAECBh>#U@}Oq7mi?;i5<2dW0jB{N~!|=&6nXDi!SYt%=(lzK&L}X!Gt!Z>IOI54qI02 zqoSzRZc;zYhahZR=nN~>XM3FIKN@j^`*Ih-42H(Au8r#KtijsIb^eaOrT2mP`dbzV zqm%W!JR#)35Bjbpt-HVzuKyQ0KAr)D7vnc|Q_EBm7vlMejjIeJ^iF5y@ z644po*-N_+_)>$`QAw3zW+4(G)iy#g?GjPYOSh}T7|SMGr!q8g&bcjGC~_x0_xjt^ zS?@>1;%wi>o%|q4KGTP~tNe?m-el*?aGyPYEA#pn<@ZE`*cb0;+dc(p*9$%<+6pV% z2faUchf0=DucA_#>>}di?ZQ_BI^s9gbi3!hHTzFH^%8}T6@om;rXTbA$z=O9HNM$9cFsRVp5aHyDU}vHMa-Ob)g>1_~^5 z49}_vGe@QBh-h|6oB4-Zyv+GBDZbP3HXYY<4yM&hU2b7J)fp}Tn%8VADga8fNg5s&Pl)S?q%Ay03^I{F0AvWA05xw7H0h!i?GFJf5j8)-uDr9lD+Fvsg1J@HDfQ}YvyF~o;>m{`EBQp<)3c1)1o}Ocg z>6_G?yQ(ipRJ2F_D=PiR+x$PTtT}(T1tc^aOW8UgM;|fWk$^p_Xqe?zho2u6un#Dj zm%(1mcrIkSjVZ^(w|ENrocd|6ku-@?X$y{9qQ)}ULPJ#WpI-eeu751_#&OvV*Tt(` zaV;Bq?@3!Zv1`&^e7r>#-8Lcp2Q5AD2%&uB{0ZXK@MnT9&1|h4#OR2E+HUj2WVn%6 zaY6`}d;zpEob!}K^u|WN%S%JiVeRJ}7njNxM$#kp_S*C}9Sna0ml~D|P|cYZ7r2#e zXl6eG4;lSNZjTN+xK#=z)Vgh%ZD|wXx{J<7IrdWgoc2DbUXMu+^O43qzPwI`4Kq1o_bLCvHP_8hm;xUW;)Y5Wg#H zKU$76Ct`yjJ3rJ|Wo++%S*`h5DO@p3`JKUPVFPfe+RH_VTx2Oi_&dizoRUs7Lq9Hl z2Cc|3Uv;JlgQ&P+u{)b0tP0j8h`o=O7+cG5^?$C~Zlw0R@zxr2Nfrn>NY^tz;JD~l zb3)iqX%Mm%432s?jmf{eIM(j)^W8-rFqU=!2Sl1m840^j>#@*h7XK?b{Flh+Z{qtR z_Vcwadg~nI(hh)7OMu1=@LH-z{D(&y9q}33=)}r)ZzWK#*#Xlf)-!qBWO~#X-2_=% zRh^mxWW`oBJ^*4pei{1_-i#luq9KtSFS(H;0!L1vUx4gZ`bMt&pP|U1tNqm>>PJV{ zMfl8h^W9)EEY`Hrt>p#3mZd^c&>w}r|F7TCJ^S5snhpAGEHZBVai#xzvH0@u@gi;t z@@we@oacW^%H$uvJ@qRm!w41w9k4s`M^V;4eiwNCH{PNR0H%LY`q|%J;vZj&YUY7o z?t@x2V2s6!?T>@Kzb}KQkBVmYP#0m1l1CMHL@|Qz5BsjaFPF*wmyz3{U`p9~g8JXk z9nh8jRuc-cn&|y+$nzc>|JrI0ioew2FSYo)7XPIde>ZM_S&M&I*8h-NkkkYGL2dm8 zE@-|#TWgeSp%EN@p~YdGWgxKbS@tr$jdZ+F$XcAPE5z|*rRm>16+EGG>u+2De>gm+ zq&cSg%fdOU{pGd(@>+j^nZIqPe{7+@z|22z@?RMD{~pF2UDqV{9MCdy;cGbA$kWD$ zuCqUqF~^?@Y(JJFYaJuK1kOgHv{x%=3>)K%GNS4WZh!fnljf0tsuLaNVYdm5q)xz4 zlu=Jf5Xb;!(tlzThli=!f}2R{yzye9i=imxy*sJl!rV;0iLExwS&aHh zh^g7)T`%23F;%C3XK8iPzj{mLxX7WMvdb&+!~b+~z2Km~6F_!u1GCapNk^`;{~Jpy z6sP~yZWW-H{BwIm>S6?5{;fg+;7t@SjtedT2; z2j4m?=&AGQ0CQc0UvYD#`vSfVO?ZBsxw!}Y2NC5OQw}Ou@a;dp-LYNuUvqw!jti)c zmuTx`8BOX9e@l-|js+YK!}%!Tug^}!7Fr3mWo1lzI+tO|>1Ec7@B%Pr{N1^&avz)^ z6T{oE+cMn&Rg@cEh&6*h@xX1SSa&9kj!l~y)mgq^PD6gKY%#~WYl03Xo3?BERs4*72*99RYD3D-i`V^l`w07XoN#2r!R5d$vQ)G_g~Y7B6Q);qk@RLr3O zV=%F>3-Lxx0cUFDA6ltO^Bp+*FJBwA>8yRge~96>6k`gk051NY-|i6d*suBiG|=%c zZkSVX4W-nS7%_VJ$~o11Ui{Z1cgUmh7sr(&iIERG!`B(Y(omD($Q~Xvw9lW@PwW#c) zn_@YCcVMKhldikmdAQG1zf;BUMw(WnJ9Bdtc(*xSX_cNq7v zkm+Ou!mwAk`2F&EcR~(sFw=hmJI^S;t6nA`4v+<85-MB#gt zau>|ZYm@d4{2=cSP2FX=pcTCehrBvN1@CzaXZUJ5%w{6Ba&sBeyp~8~=!(V*Ly6~F zJRCrU9)wHXc?v2dne=uiCm+&$<^?dcN#6Z6UD|E16aSr`9cf(sg~c*4w|~&GQ;zZ6#SkRIBWA{vr)p4A1S$EC=6B`;vf(D6t#v zgV=sqadqPab$$m}Wh_%GjbKsO_#e48SrY3AXRB!fH7&!HFhlyB4rXLiL_8lX$p}4Sm@ziW zOz7EtB>t@AnX3}j1XGIaW2?lQ8reZAhXz@=1@><5@8vJTFQSN@Qk8+8xx+3T}A{v)>>8fh=<_T^#L z^}19J<=t3`us!0Ygj7anET6`+XNq@SxbNkh5H-H`ULOT>hT{um)>in1J2n6(mNvNo zWdlyXTnc+HCnrxbs>3t4yk0nA6}%(bQ@;N+e#LWZ{X)kVZ#@t2_(YyNuDDxital5Wyvm6 zK`J{CLHt`kqjHn~Wurg%AD9)dn>Cp0CGf7lQQ6%_uu6GV0LP--PeJ@Wv@O#)*IS<8 zI9w+4!6=`L_=C*D1&ox|JVr$#p@yphG;y?mK8(eI)mr@G=*9WUD%Xw=LLCS&F=YO} zGtN4XkyYvH`DFJb4n?nP^KvzvBd3;oa0fpf`8< zoKcnIrIMTJ#-$5sR)pp0y28njwh-<*?v0AOvTGX5VaJ+ym>@td+l`6Bnmnh#mB>|& zAk}T)lqT5S^G*le1y_Yt90xk)(q3svV+m~dm((v3bdU9ci>s5EdBz;70j~b( z%|0mx{~|v!R-wYPy@Hn9OJ}xK=}#1>14I>WKbl`eADlHQbrhEyeV9GhSK_05(PQ@B ztztm!)ha57mEM~3<(4lPH-(mdcP)+pmD1L(2_f5}$jyVoca#J#7R^L~QOPqwA$Tdq zcuBjmS7&eZINluAvx`@9eg8_t$e4i}az^(}zYL?|*2sCuhu^Nan>DPnrAC$|-kdXK zRrv8`5f6ydUV=(6-;jlb(_|Q*pdiC#9uKf(y67*mgsWZyS8;Ljbb(zUKX6&@hv@|> z325gfPFm7HdAGKMY@5)Nw z+g)&WM*vcYGk~XW4V(zv9C{-t6=m;iQaM7`HPg%sK8_(VZb+8vCd=wSOESO4uvZB+ zE^&N7n{We0Fx+S9Ng zX1WNV1k!39Pxzt)!QT3JekOc@_V`auy^c47l)Lf0GdeH$G^Vy!CwuDDjtqjnbV8N> zsg!(phi6ZgmG}C#gjGn37B})<@`5R=(u2o|zRSodvU>;JRfE-_w^-!BkyEm;G;I9b zJJFrBsptdihH=$oW)Z-*_@Z*GSPNRY*dsiZj3m9N+Q2>a`cUcAlG^`Ls56U9w>a0I z5r#-S*@_+9^u!SFx1qd8`03fDrMx%%3$p$8iQe4|VO63oV>NE}V>g!&Y~RPLfHQfhWM#xt=ejlU<+G+*ZDQsz7%% zN0aOGoy~-E(_5|W6vW@H)>4G`m+&Cc5n9Ly?(_uXvvCr`1Spy4=*K62q08%2t*>Jx z-swH=!~&8iK>ZN2=r>yMfOL4L6S>wBsIsLK7DK;e)%a^pPNspN5LQz*VQ!D9PZu^F4)#a%r;+DVlt#K5>(^&>!vbH$hWefV9gX&!8`v(INaa1U*vc zI@u)Y_v0SnG>pz4fW)=$I-NrR@;se{U772F#fcA>IoNhaaj(`TBhP~_#1g&2ec7Kg zfEB%dwf80*edeZSmJo&yp>ie8!|Iz$gwe4KVYpLUY@UqTCM^R;)T!K;P?Il*gnXET z#h;z;AG-4TgOeT~bK+>iSR#MISlsbMhTdM)!;jyBT;M&B3xt61(T|JbjTX-(@Z5(K zE_fWFOa@mFMv4yps1%?%G@Pfao&p*yq7>M|wsPFB4Arst!X`A>aBsiOOvwaK0-ku17K09&lyn-(Y1Ta6c^Xm(|v?bnk zJUwoE1&mC$$FH-?){O(vp$E4%s&-X8QaKk=eou!;Lm`{n4Mo!vCoy>yq~M9L?}2sP zUu~kBg_M=%zpF2EA=;>@EP#&Oi>siaku(EZHTOe0RFwz#{=n0M3Qtwv1DQ&q5)U%d zJqB`NYETzqZuS6Ddmr7)Llp?nCyiy?>=~+fko#@+aAQ;FVFF@SJ$f9a&eID-AFr2m zj{X=qg?&!$P4x>PzUpDGddx~C`g2|wdMvNFqB<6kpF|r;v0MeLob6@{n0gTak zhtG5~*yHqb3v0dlDpzBjojN>cRH%390by!=^GJ!yxH*4joq;TJ{q?PEW3%O&NpmUF zd_~XToruP3>*%SN1c$3((?VnG1BF($50QX%uKBeICHYY0pKPuZp!nkIbQO zc5DC(c%{y7r+vevZpWAl;TXXIXt12?iDH28BfEX6={a|_pzgwH-o1A%X#KX}3w}55 z0+xbG<=HTqXb9zQN~HDoEtR9CysKJ)TzpXvfdsy=ABR6ycFO`+gR{Zy#=|C$D~sf6oHxG*)v5P)p_vXqHCOrf`;lSl8F_>vK=l?-sar80U48|t3Gz3n@qiR9N5oWGP9yq=c+Fku_9Str zzv7jAN<8OqeJq%4W0$JtzP38DI9RMR4hi~EvrZBQaa&0wVZD=jIvhXbWgmu?MVfR} zhqKN^KUx6^D*>F-L#_9^$#Fwjplvau1q3t^tm`^M#iRS{ohoYtoUhH|_C%5ALU~pa z_X7IYLvJ{Llc#w}10MQiUW^Rr;3Pj!_K7B5CUWYhqA?*`gU1kD&5W!=Ino46DOR@<*|J9Z5F*!k$w>oXl{Oe}73ZC=-3bSNiP~lf8 z$c_P>YpSt{+gGa_uM_QXxsh|%_1x_bC1kcQH!w#ejY<}WLb7fZ7C9399r^~B^*3!{ z$!6Ma)6M4+>UYl1hi?ces+1Bfwo`djlZQAzLTFNt06VE(?^Xs*jI{?Zr_W;`M3wAN zCOCcbI0E~?%CqJQ7z08F~ZO+&>4*6@+%Yqtj3K2KIS^&k&}34eCM?~0En!X8l|_{Mzj=eo)I zdJJ)NmYeG7S+LYEH{R;gZHt3a74qmT<$vNcZDca}+4c9$PBbZkZ9*+dk@0H*6wc~Z zaSzA__E3mk2oqfZ*!mPf>MYDFmq5vmT0xhOi2|q&#lfdyeBXkAx1sfTqyFb)na0y4XQ^zp8L4~?f~`|VuFdPr|s1~zwPDy+w1Dqpz#CmayvjT znvO;lY=nSIsbO^6;9agzq6C@$l#C2G%*X}<@_bm3n8B2yUvI%zr*^vTEM1q-ca&E0 zF*6=ul)IY(NW4*@K=IT$PVPPq*aup+-xR>b^B?%^HGb(in1Y9*9?W{s{W+EXV=_9S#c~MfUF2k$_Wn8W23+DoaZEpV-P4CN-5KgE zbZSml@DeFVx-(>K&8+nV!uzcrXxY9*3hjU5GXWxkk1T$o_YDr5Y5;_xgd*|SU)vt> ze`VVPBXavg>5KEXhqqG}bfbSm;K`1J^*p*gVW4MBjh_U6dJzKo1c${WxnCAIXHygY7JVyl)&94u^gkO2z}<2dZ}QNdeWIrVfaHnI0@lEAW}t5%Lw>LJ$gdz7Gu37w!Y_P}T3i&m42PHp@))6j*^Pk8h+f(w#{H z8oS!~n2R@xj=XbEAJ`4}pMMBsWSx9$Lhl{prQ={RbVG60>PYe)=?o>D?Z8dI0su=T zM^BEO@I_qBdive&?=QiK%-~iNZfz;X#d^sqo1Bq;XWj`2GWkUl$|I^eD_whD6&Bg` zYR(Ay691>d8boa9D&UQ~GD^#)J?J*|L&awuG~9>SkVk;|;;8u6`-|9|~(H zT^(!0Kd)$f+E;J%^?qO;M}B*smFi3tsaAmz|NeSXiKeIw2*@E){7#5!4G_@7DS5`g z9CE>JXtJ%Jmnk27Ykm=}#^V5NebMH&n*S(M<@SCbexN$v zB1|pog@3)Zw;-KKo^uW7fao1QCBm^$70In42H1UKB9f7B9Fgm`xlZuo-p#EIj&x2~;l-{M<7c7mDt)0sRGNs(vNP%LNl zBG;R@*}l(=(Zik5b0%A3H@A|u68AoEO}Be~p$-D2t;l1~p#!s)bE6Ic*4EZ%IG_a& z^wOgIH9kwUO@F#^dRCso1okKZ8k=4wmyll|Hqp{HaDhq28uMdt`KG|p~ zJ}n-{pNy;%Ez@zXU(&1Gh?#z$|LtC2j>%S%<7kxzS~34^-)>gHsByVT#<(b@KfAkEHKYm+DV+K$YAh0}eVph)hD_u2Cu<}EErWZwYCyTs$Wq6Di47!1bwy#wd@=9+VUYp%KWUi-Vw zKJyLCRMtjs%7_K@#f3o0F8d`{&0rO^2O)NrF6?y8#P}3^yI0EHgNa=(z%h#q+N7=WtDEz&gKi?+tI=SL?;* zF_DzvKKBX9l%DG~9woAq=_N)g703ESH?S2_?Ih){TrWR-Gxq7AJ=<=vcXOkex-~ol znK>qfq)HH0_|czdJZHZ=`UI5bRymUT3y0fx2F45y8Sg5jd)~CUSie(KsD94W!E7pT zzl=JGfxYM`X)z4>u-1W+RMaMOu~Z?vN+B}LWA3g3jGbKYKula*N679O=}IVqir*+Q z+2P5}zJa4q@um>L`kwXlN%~$>RSs$_;-08eL|zhlSY+em7hM(Dv1uMqV3ubH2nFAg zUS!L8?eN)n2g z^^Bye%~*%E!}Bd`o&#Ukot}3fH@j-BhRnIRxH=ZMRsB`6j)4Y1TX9zbwID65&U}AC zQXSdWUTQh!y4$)@shh>gYZd#bNb|14>M*UA&2S^Z(8>*!xA(_q66RbElx1fF7)cTw z_AKkOWTsi<5TUXBjCF9{>ZjvTN;w_Y5trupTR(hs?{Hg^+9t&6hn3u8kaO!<4YS~> zt+<=@0RB!I-aM8vn_mnIrgZq)k6xXJ_c=(h{J?pq`AX#VSFN!GInNzZYT93!bqOE7 z2Z$`%sEv}WvnD-$kw#ZuA|9gG#`%Xo*5Q+iXR_-eRg^?(9kf2bz*vzt<-8_yxQ4zo;3y>^r6ZX?wTr{?&^}kpzNSD&Nx+0 zk8wJ}gxyzDujzabjb(1#@*W-f5j(hOy+YbY6^D2E#yva)Jho^|II(1NRxuh}&7@sh zPh?p~lDKdegUgq!*4f^AGb>v`2e+cSoqqB!w9OMImq+2;c^VqBa&HbvjDPNqe!l>J z;MeenC03&RT7QZlNl8^9}Ehe{N84fiL$? z-0!9mPg{ne5Xj9H{)v{ejM3b_O8_9|+8e3?QykZZ?h;V!e}t`l_f-tCe}O#QS(-T> zrFHQ9(v<0|C}VUl$E{ko{ya5j%Xvi?lSI(SIz~KORhz#ay`SmOzteCj_x{e0T*#tl zSs<<$-A5i8x36csmK(W|S{1l($-S%KvCb;>Lnk5R3t!tA_U{tdG9P> zR%CSbE@FQLx9qDgg+FaLF)-T9a&j-cgn%#i9%i&R4QZg407obqBj>}|3oIAB1yPwN z;>10Q2|QYFRSU1oqcSA@XX^JRVmb9~rvn#Q9tp^h>FjFQK0)Rv^0mEHVIOiB8k#li zZ3w%+dv;cS;;jmc;c#%8V!lqPiLV;YF2HGuf+<8hDp`ir%4w7&(h`^<<;9PCJG>3f z%0~ATT8nQSxH`!ChZY7V4h{fFFE)`^F>Z~{7ilrf%a6n-*Q7ahNXWaQUPc`g!!Byo z9cH1%xDiIG<@6mH)qbVDbXHc+pR$5igvlwuF8ZRJpBF9IbKL)Q>~ommH7;}+CYQi9 zH?ymgHt@i*=70~1gx1xnRw-L4c|}b5RK1bqdA1!gTRLtV;x)C8LoI57;fSygjk>A9 zCyVl~Up~$Zfo_(T*@PP1C59`hxZ=-dobr?r!7Ty1XR=h1jPA-Wo0+YExs|P5TEbbf z3c`Il+4ogilSKN;AB&X9U13jv7DUtSOLlMVW6wTb=QeEToeIw;0^w(7P7l^EE;7-_ zpU0F0J52QKKkb$zG2I_F7hc-~p0#bGIgVR0<$ZZ9nCV%xP)?^UE@J(bxrtpsjmELE zt}&Hge|TCcB8 z=jBTntXFDmW8=T03N@;vCypIQ^uJ^teaqL0t;_Sx?HKV9=e4XpW-$F$T3_>nf0F88 zxlA!gLGZghfPiQAZG1iLp30X7?mI3IdVD_6y;}XY@BVbtT-6-Tc*-zIIj7i>`y8+L z9?qE~d9Ymj!dS}$SjX4B`1ZNkmhrH(fwQg)#aNR3kOr=tXOpkitq*3vlMh^Y>W;e~ z1)}Wk>US)-PVyKQ>q1xsgqWVdqaBfwIN)Fw1G{|sSz28eS3lu3;J%)-PM2D(i2*sJ zwGvIPsV7i0oyNgAy1K28SwFj#a`Q<$HA>B?7m?LEJyXG3d_6?3C4I_fwFbZQNY%nJ zD47i9V;V*+tp2QDL<>?YQ;1+^ViH(Rl}A0VFE<-$-R@3AzA129bndh4d+(#^xDt{h zYO~D&6p$SW%PJJ$X`B{Ci&ag9&R@DSae(DA%7Mr)z4gZPCXSTa z8o6V+hhNs?RF5tjy&dvSF#=}emaW?cB~~7IJtca7O6*VIjd2~_HVFRuBed%xUZrRf zcEklsS3vPVIn|lf9eo_qEwL^tBXDW>x<4XbvMVX3=we*Ko zQ8AA-DFx3Sr*Hr0yQv%Dyq$F*>bf~w6nb+7(R}5jIG-_?{PNv)SFN1W`iKswar1T< zcBKusho<=ERu2>^E8d$6G(W2wAsQ9U!A^0$sKQ11=7HEKVq13$Ep%#qhok z5-XV(dSuwqzye3(P&m?tUk4Ug=7F4#7Z*4@rC!r)*pc9jT|XOWO7!3!hQ)&`&qsYF zB0)%f{T3%aRvWY~&(fmlc~@g`!O?keEgj3@65axH`rdWJdZu*hlK`wx`oa+{DNk0I z`S_7c@>;*lN|+dz1`s(hQ3$dcI#k`Vkbe(Ui)IuBMLUngQM1hI)o<26?=9dL)ong& zT^w!DJa$DQHYHZwztFa}wM7b+ktFHuxFdG&53EvtKi;|&r`@Zzx;iw_f!gjrQ@)QM9DNCddTo-y2qNhKjN$N$e8DtC zF2L69#m)n!eN$?)>an)X(dsSIpsaP(Gkza$b4ku8nPiperNv5qne#BBo zt=VsOKR%`<=W}C%ixp=8rGu1+?*~yGPG4?2EM@KbZp9^Cb29q`xjk9-dKw+K1M!-p zDpWUdM%)dRw!u(RgcqdynnQ`?H0XIQxHsxIXERVl8F72(Zt2D}es9*sWt)Z_{*k>3 zh4?Ck#4zO1YGO|BR1rm=3P?=aW7#7rtO_Ewx-4iqt*l6yd&I|sk^0~6Cy3Fp_kS%n z?$%@)8XC$$Lec$ZZfqOO9WnEM$uc^Kt?u)-hRATGS?an0vcXbK&L3R|XuOUgNhE*; z*&FPEML!JeKw2+8;#2;#xJ(RQd`tvlMUv@P!P0U*zxu*U4;;qtlhZ4F+ESGD zb3er6qejZg+GV%DI+>Ywr@DcWUsVPmvRwGu09*Q6M9%z)nkMFkO69plLMjoa3Ae2n zkrMNW0&S$H|Ii)b0<2?iq<^craSC0wY+R^Al6~Q&MIEON!CLl*4MG;j!#Vu-6p~Rj zM8_Yzsb_X7%mT;C?27>zzO{$8#%8ni+lo6h=Ez}nlvclaz71$>U4PPRorXRJp?z4V za9@FBqvJoFY#3s?7c{8es;h{@r*>yj<$NEdIQD+$IyarN*zkVFC2sDjXWyb`qm>n6 z@X5u(k%&Fv((bOVSQq1?`;~5$>XlX#t1bPB<}0;!My5mi8-6bt`-Ne;!QSZC;Ka;5 z3(P4(Si%~QlPr}v^NjD|;tY)w#=s}7CF0ex}cy=WSma%d-oK%v3@Gb`wL z0}dIBl#dS|a{zdqB03{XT!Cp!fXM$Iy#3o4&qdyQXOE@FQ>b?c*S){^OGELy01diyG$ z=8Hdjp?_@~z`LgJGKc52=7>TGsmQ(M*3FYZ=les6wX+TbB;U}bfgK(_ZzAY8(4Hq} zyY*);^pD$+^i5CX7cGGQZc9!Au9mL`eN>PEr=r_6a6SnpHki`)uxposo}rTl7qSuw zTm+00Z^_h0{-~8dU$fp->2SVCAtl+Iqgf` z2Ks-vG=K0ye{{zFaZAwKfWGHg35n*z`j7$9|D4LIh`#FAss6s||G&EG&gv3iBsl;5 z>2|g_AgHU-7TbAo35BguWf>87e4JUg`2%&ioldy6&Tes39#m}^8B-oT#KXgz31UBH zLAfA#0i9K*q_Te!fO$9J@VO^tyVlJyF>A09Fyq|qhV6W3B?d>-(t6gbxW!K7}!JoQi zJ0Yya`;<7vH?f4XXy3;Wb+*G>LIiR`Yn+ckMVAYNR0d3(XRr=|^E=cFZ6=Zio;UnF z{hWjZH7M-rbZlJ3Y9*2&kIeW?fxAHG#kUY7NF_-_3Vaq=??PI^Q3n0Wcz+WsqKTj+_dm)MOHZh?TWTmE{Bc=o1=}{i=ptn zabd0>AE(YX7jyw`;Qc=F(eUR-&D5UJ7YNTsf)P^p1w3?L2W2}JV@5_qL_Bx<_(?G0 z_TlBT&8$ghq~xRf6j6qsHd1OGq|;n$Wj#DRK3OxSoYW#)ap!KQL!OVO9|R765ia|E zeG+YS|C<8sT}tCA4=Fa=QPZ+l^WZ6X>8us+DF#!ICI-{TVJ}D;IJGvVpXebS)X0A zcmq^imip_B73#&;>im?JazE|*oFTklMMVjk=dASQvn|J5B3)5xuAWd?Zil*nJ9l!R z*t1gWk+{VnvHH%YtaW97%$A!xgM4dJs18S2~^k>yJ*Hht=`-1Xsv_ zyKW!38lN~W;V^s5dKC=#(_^mIjuzs4wWHK=EyK0(`pPx?X9F=2HPiAD?^V*p_F=Hy z_;1w*IYzyi&SrgkQ+_*KAa8Z8Q_61JT>qSx^Xj-KL$VjvkfL|_ZL`Om80=B$wY$5j z5l8L~hd~whw);vXLp5#fm2GQ`I$1W?XRN=_ZXrLs;#4XgFP3v{?LFAs4m8}Yw&!wB z8$Z92ayp?>g_g59!v6&u;S!K*$>ac=f~XlK16`S}OQ} z0@bU6u?`r#VyVb>9?*R|XQ9laud0&*H^i=!T@r2)rxn$M5# z&bdrZj+Z)bF@mjYX=@8iE`Wa?MTw)3nl5{bSNK=AZ==J-T=kD`xUYbt{eB+v*U8Mz+P|x2%p$$TW9>Io%o# zDs*>ul&>}J^c#=8cf9WS^?4uLq5jm-tkQcs(;-s9DHtf-x;h!G3gl_zrq)!hnvKC1 za_#eXZIJf-39B*@mlkw_lJ8i#TblLXP8@UbH|_Q?Ppqrnw927GymnX-;&Pd}(`T4M zW0i898rcz)?54wKv+3^P0kd29$~8>6Cu-SoU2vuRNlx(rrDrg?txEiDnkCTrtLqzeFBY;AYd$4eW;%;SU8g4lt;P7_P{bK)+5Te=V;nkxB+ zKZL%BH`q>H^?{JRa@vylsnu+B_P{l2g|m%Mmdt(%LCDRjTC7d)tV~^5qxt-!Z|~ap zr@kdd#RQsS*sh{0YbEE;DtCjzyRa*BBsUAfDhidd-_FRVn!b+}uXvQ|WD{ZPxYjS5 z$OHRSzGmWF=FU~W`0AkKA8|Kd#wm>kFrAJgx4qb1KSVBm|7mbzcc)QRv!Se_0Hvn)>U~cT(rNyT3Nb&vDaL?Ggv`k9&5rWJ zo$lBvNvVWN6%nh;UlBq*SuXr6-v z1Vu_9D9YmCJd5=Mc)K6BLmP;s13^$k%)oF$q>gWqWf@ zkU;X+Kpxuc{OBd~mjxUF3nolC?^>Lb2X|Td#vr;sSESmXhaz-tYem%4Jf!4 zxDL2x$waYqR@md!*^{|tDne2U@6D`uB*$Frq5=DVxEvx1C{zIwlcV9xf^LAE{6r>B z1lD1P9C~a8thjiwJ&8;C?NR32nYE#tltPt4nJfDxQQ1bFeY1$QJy|edq_fTspDi2{ z>7Te;Fa#!E#-%EnSfzXxH?o@w*i&TFUAbfASh&^)dIZ?pV3oETDZ*&O&x1+>>HQ9G zFELRe-;nwiL^oP1z0%l)r(xGJ?p3o3w~4OVa! zOA$eQv1}eqc7guAm%KLfA60R+rGC=|@P(yt9->Cw{#Dy4>77U(~p%ygB=}Z{XY=mnDP4 zJ88?y<_V&<8Kf?|HJ7mcoDu$33!DXP~DMi2(N`V1yeVNLkPFh8pf`YKojooUM&u}ms z5Fr#Ck}p)qV`<@Grad{9?|zGXx?#eY>R3k~x6W2zk_F1BAAcA+kBxO3l$)f}2N|$K zIWfzpuAVX$#KnFI%3t9UCGxlw`dB6noTs~Nh;VVinWFL5OCl0z0#{c5eDW*}5fSi) z5?GCVw9kW2pt?|Nr4mghfW7$XlfH3x-D?G7-@;}i&kke)<9_%uV9Ftv`rLxq*Eh_U znr2A{bUcx~3H$3;`-)WZD}zb3)ZAb{X5$Qox0%mwi13+ZBtO4h`SWg-&IkV%xO>-} z8It2!tvy_^V!&qH>G4Pvwq0jhWjc1NsCM^WAC;g>d>!hz_TeQ&?QP#EgQoJDxnZ)Z zxx3;)S41ugnu;rfj6rsByv(*-cohMoC2SEiPBT6%P+l$UTUADy?8~wo<8gmm{-D4O zh2(;;eFE=Cpk9|3*+76b1ywSIVNPfM`lFMO+>u~9MHmXng=dc3AD@Kek{~2^SBP7+ zSrx>Wvvyi@txeF|%?w3-CsCP+U@nu}Gc!|d2iT^(Fdd!bT(`Rtn5BEub$6y+i&EnX zZ_Io9AFVB;);XR(BPmw}^9x7%wnldpDm}ZXg7_FhJ?|)pR`}I)94NaEm6WX$?^Pp? z5u&Y>f;`XkJgdw`9wr`7owgN4l$dUQZ-Wo%fBEj7|JqhTW2nL`SJa4?b>d2ghEd@1 z*47pnve4ij#4XU~i>`S}$zb`>hC}98dXi6z$m_#HV3edVKXYJ5gX(JZX{svP=fvPB zJ;dNVkC_cho!Q>a7Pu6?7{kwrCC*DqFat|(I%!RgB?ZdU6gWf;L=Wztq<=g$*1|WT zfa#_LCT|WpL{QpOBk8GnHlzSRYoqCbBos zN@W2`LxecNKCR6sqy>n(O}+ve>WFdp;s`Y52v+<&Wg!d1_|YtE@HWjSa;$udZafh8 zLXJ`L-8blMO+s#ZNYYS(J0-OJ&R4<-oVBTto2j?{`XhAd>{AllX^sjU-AO7@)OZ5A zY}eYQuO;F6J%F(dBoh`o5E<}jNvF6iu?6+4UqC)_Vq6I{gjqCQx}5n~Brj4#XonLt z8$~CS{g{m@wVB<5Fq16xGU1u8%6N)lsP93j*0FkdDKy?soV_rleR5gBe8;te(&EmPe`-i)vTzffQd>&JzHSrXxi)f*GiH z>E`D-R6`|aD zcE)~SoT*S_%#h?Gqm3JC4`DdC?pjBybGLf4sDMCL*s>~s-sTgh7<#ATiORu*`LVlKh0Ii^cuG*S6F#( z(=Ce3vYFM92K22J#9qXw1B;Q$HXnWJ@UdEW83+Q%cmyVh%Z!cVs?IGIVr^S!h(}HB z5r!r&!>Zl{xLc%fiN8Z}2rd=aW&!G@11eSYVD8Q#iMCtUs_fTy3a#+U#gNFD9b4Ku z`zOasW&{%cwZ}#c?ARr zJkbxDAd>b6AtiyRIV}-L+a#UzgN*-;M`#FUeU3e~MhdKSwNy9U@x&gL_T^(8su|}$ z%Dcg9mdrqHHLPa-#jQZTJS8E4dB@YfvIJqi6l>zxJE4-F%9@*O^VS?3l;sJyxT&z2 zo`XZ>SK;Z$rL$$7XBBJ3hdY1t7cypZ%eFU#@n07yIX2mC3_&xJ)HP3YoQ9H76G^@9 zMPGnR|7)4&KTA*1(P8~T$b23Y9p?3&Euue(oa;RRuJL&z`~=%EXo}*}NGw;>A z7~c;X(P&?+c-DG$wiK$pKoezW*}KJV0=8pR%k`pSi=@!rcOMU$=9RMVw^oh7Bk-v` zX!?xBuALz`BI4)&cJ72p*GaI<19`&4>7X?t=@{VLZU`>Z5Lp6$S3Mo%_-{NyM-9*C zImT-sE^)PF*M#HGvhPdyq2W?(&Mn*%Wfanj{qpwa_1G>d{3&v1+_2`iZ>f2+T7O4BGmGN=h zX;3Buof8du6R`T;)gobnX)cow&kuHr$=iPN=*7%MhQY{nii4vqi!ZWU?g$IVw!$hN z{*@O#I8av#R51a)od1B-(KR9_nuY*kqG+>5&C;K@(Qgb!l9e9&*aIm@(L5cLdF>ky zP;Dp$Rfq59g+UMLnq^aljpIGqtN17W3#@w$b-yXynGnoPNr>tqL?4y^vjHX`U4n{h zgg>L$vm-6IN6KiL@b#{}@xAT1Aa>Z-ZG2FSWPTKu!lA2~`t{92<=wRjc8d{`D-F$| zBm=34eWlhLN%SX(T=Q71zCT6H+>e~5(Hje%PMg)$8loI}!C&+DSo*3A7Tug(8o69Y zg%V(jYu-849-v`0vag~ZWdCnf^4X%O5{(Ww_m*RU=vI&lfL#ir78!%nuH`-|)-7Tb zW=~d$wfU7U9X7zE zj13b%mi9ZpLIQdM8$n44x-Hz<`t$vzu8wX~Fy5Z`Dy}uh?~^)`|L*|c!1a19c!-$L zVsWfk)63~h;g>x=tbz|RK%8}_slK`48%n6%5v$q9#{FT*L^|4hZrd3J98z;cS8{dj87}fVWC-wJB#?f0UQK`8g^`uGY0m8)7r(xJ$JsT^gSt zE5}0>ien7cIY*tR7;WyT@?cdj|CG!I-womA(*5oOdOKbN?*293L*KiHqN}T{0N^oP ze)qcagrKVQqLqjPT(46ZfTa9Sdm{g(0th-6#7_tgqlel!qIt?_Mggx8(7ale;g@@$ zdjy(Ho(A@u!^69lTTvAWSnk8-&rV9YLjwV1{jHnecf4q9+?gb7oM&KY*jj0k)(RXN z0~fEmEQz>2tWfW$PKt_aaDkN>mFs(f{%(7ftpZeWwj-iUxj+qCE1z*jff|j*O`|-FJH^ymUAf zpq%Zpqil#ecJHlpKgSK2FJ*@NJr8p%E_Z9(-3|~l?$2l63u6*V@c?|C%{b%7*ArtO zoiLeYvazCQh@hgyXTc5}Y?@y;vzhq351;11j=Tn1DSQKDh#p9RRtgdu3co)A=FlGg zeRn4iRIcl)@-(!o%hiK7+4im%C+2P>gOGwZ@f$6 zvkw(CJqFY@M#sIjlC@u;dr(+3S`>d5H>OiwVz2j>Z#g-Af0U<@l&?g5y9bB$+xBGUMZCMPCUkK*7Tcvs1=)Rt4IYIyV8xucQ z%y+3@Q&(?p|4EuzDKU9?{No$D?D*GD%5y6p)5!QWJ)_E>4UKE=1SP{z>NCX02I4^b zF>1P<(n_%k-ScLt<6mN}k{i=h*lE>yuFkcB-9+DQAJOqi{My+%Nr6cX2urJ4AG(U| zCI@=hte+2(VT%AFwG(byHti$;(0syt8v1D_X#wF}a-S6+lNvC>-6$w3Ph%2-5S!|g zu_^9;JYa-RPrl>Db%M&c4-hlDFG}p5LQGz?DsmFkK8pW>-_)Lniu7H}=K`6vc+>^3(`AEiKUGh?(pAKE3&hRWI+( zeTkaeTW*c36{;*>BhkP{4JMOrehTUaXjjwcjj)!y+Xn zN5DD|Zi(_N_C&ooUsQc`<3L$Kk$bJ%)A{}twi_ZFqR*UWV;;*Y%u~(vzMnW%vv=43 z`Uxn;s#}JEzb8uo!s~lIL6oK!6(&-;t6KDezxzWLl08#`-2=WBo@Gl$spte z!pwj95gN$3oz^6<0CjauV)0E(M4sfMO6+(GW&=fZHH5)YfbcTC1&l+G{ae?}ZpVq0 zNXK#acB<0A36d@y%fQLPF34@gX}jEl?QeS899W^!m~U4FZWea`qvjeRqSLCzu;g+fL1`Ke*CppnieHiv-+NVHuD@iC$eAW$YO+7(Ww;X z{`xxE3B@J?SI%4F03Bw97oB$0! zs}Lj>ukVG%2~~7Uw@n}?*7$K|OQOhi9BS;AT*9=Lsf4dsC?;{u-^+KWtMQwCyuJVZAE&J@cA95^AMY@uL-8o%6$|ZUGVvltaeG#jUv#eJ?@FE&#M5nc`0a8xIbu-qW6!7}(~(Ylgfy4IBXAJH7J0 zbqR~;9k~B|cw~Sw2C-AGMPwVfY#w2zpGb4=P)L8KGSzw3Hb4GuS0pv zb$q%zmrhCqb_9~3S8CGjXP=gs;_b?LS?CqxCWnZz-9V?;y0h0H6}AWnhMlle4QVGK z3VZjd&$~2}7l8AoTW19-!jSW(qNzNC83wKgmAA1v?mi?HwS`e4itBXe#CbooDKSrn zoVUaorp8XFQOi9rFDjK3XM1sDE58DrzIk+t?amrQU*CN+M2>v~oaDNh!5|5C9q@Th z?P=d=Cm(~a*P&~G(M;Y1U$1doK!6Dc`+7u1x{HHp1=eGH)7Tz&ACxI42U^~-<2uQn ze7!|zi33`NdXE0{ZL!e*3@o{_m6j_q~TAt>m$+pyaXY zpl-T|quqWumsWpqYHA%OH*Gi+FNI@Wk)ugZ1yO+*4$yUe2~6!vwS|{nKwiM-kQj(K zUj#PkqF%-&tRKK6{ZPihAd)^wVxl>=?QW;5L6L|W1})`@f`C%eKW&x;-sj8S=>c7; z2AtHZ?#G;(A=x}o_(3)u*i9rI2M`&yTg$XW3Lt5HJ)r&$5q~jll-ELhAhoV4PvHZwtI-G483A{;SbZ!4se7yOPEo7Q)Tg)-AFrlVa zCo9~idZmWl5YMrjwhZ(LgE)!kLwboJF2yN`Bu49EIvw; zJMfmCF}gh#kgQvcHL6Z%04><;oGgupt53g(1yoCY! z*T80hc8UMh<~Ok9`;&o zeF`n2F~~{S4kUDB1&+sE*IM43@n@tc6GCf~(D@<)#2{{Nu+!6U^U?i>nYuL`O4}3v z5@rU0l>a^53V-;6tIt;52OvmS?#=YqEBW9gtP9Kiap@H#D8+T(f!0WG!@esRY0;9m zQQQ`%<3S7I4uTiPg<{4IJ9O6LMG&LhKtV(Ia)$WvpdPaZU%Q?AZ(7}4%Tb;DI&q-U z?QZAt+Aoc6HEbu1Zj2gqx#MY$+LC!Y%DS}`xt5|rZM!QI!w#yie|EarKC82@2l}V# zsE+#hMB%%5-tD`6 z@Dl({D_QHtiR@oaBGzKWs^#{-t;gRA6?1n&?Bu#C4{gEy$L@<2eSl;MYu z{Yeq9ls$tti2mdedL=v7=Q*ijp~lMxn_rJSrA|0|vOx8vN#rjdBA9<_6Y{%uJR5aW<~IFOSFwor*#se~ou_2z3@TH~cdW*0-Tj3d zZ#)V^qLn4@2CfxeJ3#pD@ZVkkvJpyYGn1H~l-SN|rVS9gupokjwKMmEBV2Yo42NZ7 zyW(;mvz9d%MlB+?sZ0Ww{%OD4QWzG)Pk62APvJGRm6GNae%A-}>6?VI!z5``fbn@d z^Ug=28rbHkQ1}XJca3qhy95xq7yGFA zb3c+06HDjwbuJ?`%({}rx`|sf82k5DJRL#RWwp=MOYrW*kE_2xZ3Q|LA8kG!kR&X5 z`?$6P?T$fxRXP?H7Ms%Ji?*mco&rStf&nMEev~wJjzl_I2k5gQ8>BA;S>kc>$lpE} z?K4XLMGN3p>8}Ji@oCC9+Hh=O-p@+Qd3)UkzVB1|G#7Rx4Je#r1~3z$>6hRZ`v!!x zn?e)zBDVZfC0zYDlT-(G+4f@xO{$`;R zkG7dlFZS)1#v?`_HpqK^p*}ay9tZNq_MakV|J4b%I#^S&JdR8ey<$;r`AxeBzRF-L z$ZUg!G>eE@DD1ZAd?H<@vBt476eb@<^7y#3T;CInDv<&cElu7Rfcb1uGq2N}Bcj&Y z? zR{SUJaOVNA;@`r&Sv6#vbJAf*p{i!3_=@K(pkz8n87CIaD&CA?Ow}|P9;@AngjmTd zgP9(E7q;|2)-lrME>V*R>UsbSPSfckjJM9<3!d%!qhbF3mX`s9O=?G@<_=~-(it8Z z@ShmmY_|`A#!1Vw9A1tj&24bqh)mGT+yA6104Q8@Dh8@|LMJ?-EN58LvTn-A)Y(|r zcFglB`w$q`37T^j0O6eHnOzA)P=7M2?vz-^E9Tgx1Zh)QjEF_KB6fm#1I*a9j)H5n zft+XLUW;UosNTNG!MB9}SC+E+poHWnBo}Q3M?QJ)ux8@-J4jCLQtj9;vXr7fWhr;# z`~1+^r9#<@1EO@VR0^OQbMew-JuN2*d^Z(@m_x&tUU%;;T^V{T4N*hFU*hr8JeLEu zshOBR=oSqJh?+jU3=FwF84rTWP-e?3J`ztsp6z}1GP&;uPLLT>D37w9_Wp39u8hM# zIrJtBy35p4Vn!MnP?L#%ta-_~gFG*ouFttFPdK7OW(9)eR_JoOsYRT6#w~M0wPt_h zdsr0#6I^)d$0c{6(HJ^-ZB8C37^@aDGrz{S(s(W{HaPgaI+|&r;S}k94e^_nxCE#% zS*h^JPJ==t`!f{igI20P%q#UoiJ6YTd`XJ{wS-j6F4|P1aZB}rduDgS&X0B^9iCz? z(4!6ok=Esa9Qtp$5aME>l=tB}?q`U$ZcSi|33>+aLSAe<{Lu}Lvsl(pWR={Y32Gew zyb132YL+LL?2r56JebqPk>yXgT(<8hS6ik~h}(SSP~ThI`yOw%<`M?VaM_Dx3f@%; zK8o9>Kb7!|-T<}1O%OmN?MB{Zk+VKVtif66qzGjVo*iDf`HHy5*r+W>p4@QV-CbU1 zJUT&XmPCafYTWKA`F6qL7x-a9>KG$kzIM(ib;lp`*YcEUJuub6VXl99hlq?2iI%3(AW8Np2H>BnHxX`9lE;*dhu#Lj_Dn(vP z-7i9Vg7pZyC`dBmWyk8`&||imtP)Z)k7nu>!Eru~E9E^bo~wy&}etG(YR?W;iJ z`Fc|U>pv@{;m|-&;O8LW&c2+ki2sc;mH$ZtRD5m$Xmv>iwi~xVGL^@ul>>Oow5>6{ z!I%Bg_H3!WaU=7N!80=}j^lCyR{K2>O7LSrFjGzZetUn_u+ zHN;Jlm|>izLAyoL!MWm!I3EpE(C!uVZD@=-D_>LY<3CYJ*RHf2yXKCVu;>oDGFz;T zES-_RDPlNIb(CifgyCfJV^=y=;FxlgOu1uGU%H(**&WN_!!a~I->;l{e+jK%|IDcnNDGipg z|1Owr=w%7D1+M<1jk8#bpgh=X+(Jo1bPq&cS6>Gz5=p0lq*Y9E6~mhYN@R}+xPudd z6fL0`!IC?;9tEpM7y#LY60OwopR!6@AP)pnwz&*@?UcV-3ZjZV>u(Usleyj@sCf1* zmh<(wpzg=@N0D+#V)Rp^%|@XtQSfiir8r6)q1-S{fLn8Q=dG6*XpdtM-G1|^@JXjr_3Ff$%xX(Uql?*4M;an4 znoa8oD8fVdpNP2!NTLl4R;xd`7P(5WcxZlpUKWh}L0mC@9(@vxhDBnZm!tth1BSytSiA=U1FuhVW(xYV zSN$ZQB$C#9#17=rzcj;1(SL9qdUyfH2I~)0$v~0$w<2C(3-+r@|4=P#UInw>KC;d} zvu@|WV5^zSWW8SIVB)o_m8sj%tkLREU0`$Jd|da7fbjN$yAqDTZ=6 zfdQHdr>58TCe}!nc%1T?M86o1!6#x?M$RdES2eMuK9ZqIIgF16qLy+xJDc~_Yc`z) z4`!bA77S6^{9yLYZH8LVXI8ZZ{l{#63e(D?inN5U`Thtpm~JMAq|#^nmaO=I97ei5 z8O;8M$MU@608b2o&?3?rVR7U&q4InlcO?aT`(W~l$@to;-^of6Ui#Q=H5 z_`y|OBW`8Ktl=_PJvVp+;*StC9Z;zqLqHGTtU8-CoEpnsJpR{HxH?E8SR@Ss$rh8+77lnks} z8S1vG9tp&Pk8dcMS&XYVNdz@TKq9D;pUkSv&@DYYa5v)iuJJXTmAm3V{=+J82sqaf z-)&XQ!^*<6)^)@gDFufKjjx|!T`eu)ztXv^L;(K6Ujq@LcDa$^`wYrM74x<80o;py@e`soWoRlG;l0#&K1si&y9xZ9@Vvot0A?V zfLkaNEPI915=h|O!94Yf=0;{I6V{DP-OGkj4>`ZY-EmW#XjoAEdWW9r+BLQzO||d1 zOaw7a-BWjw5rw$UB$M}DBJIC2{v1$ED9>~MX;TrU`q2iWOv@HuPBML>N8Rt zGAph(oNXHbT?#i#=HlYwWap8D;T3B|0d6D6t}9Dl^U8QEdLOCQS8#2H>JuB%|$4Y&F zk=%zbtJ^sFDpoeA97Mn1a+RQQ2TQko5PV58of!O0>46a;ZWc5EWwNYvyOEx;QW1RE zV6NujB}nSaeereCv*vP=9u4o<7xI8HA$?7m=oQ5mL2%^O%fb9GAauo^*%7f9dYhPw|WFW38u)o{G^fK_R{Xds4t z1R0I}f{yON#v5hOSF69UEK-2%hw8N`EEkr+5G$ZMdF!I7NbukXN$A}rt8_172SZWS z41QC?D|`{?6GZID8E$O#~X|`x58Nt6Q!u2j= z2ZAVsST_8CXgvzO`ds4rB>5ds%w}}~ ztpY9x(2b{GnEL_U8Y_A(=Jcu;WBWOv_HaGocP|Mn_W0A^u7?D9Fx{@lsZB0%tzf?W z#m5CNBp}9>-dQecEDs1w-eys~1uz;D$r9`YLQHXJ%fwi=xE)H`Sb-0C8k?S!f3oQ< zS$}W(-<$sTO-Gxv|GRIxU7;7ueu>X!rH6brzt)3yX5$Ejc->KLWkZm(<~FakXm6U9 zT=^zRc$JKWMU^$|M`V?tR*&=Y^qZ;lq97Gi`^Q1E^?tmXysM|fbE$1$GGGG7mm9W)j}5<>wU@(jM509L(F%i zsH;fB1xnfDZI>3#maf52q(gAsX%|vEd!4*DZ(c2j^}T@{9x3CstLMBaAtwt1aqIUJ zhqp2Pf)q+3ok#)67GPzsx`Rn;uv^+~ocC?~`S!odEPF{n(Yb+hDH*mK&{+B1qZ4-< zXVMs|1bMvdnZ-81_JW+w6)mQ8tKO^tP40ac!I=^NZ@NTae4Y6M?!j&qAxM6z+VhK% zd0okJ4aB8KIx8x27_ZoZTD?n5fs=cU-EeW4-J;FY$~PXH(Y3RS#;I)y*11s?;NozbY%7vr6+kRwXT-#r2c$xU_cafhC~DOBz64YxBRfimi zdr&*Y)Gg~5B(pQWE30!@S=Zxi_vLoQYB?g`x@~XxtN1R&&33MQzN8SVv%5$g>Uk8< zc(E=6wmT5ry611@Q(RCGZAs#Ye{RKx^P@*!P6u8$ab1bWdU*ab#ljou)HkFko#h6_ zj+!zy=ic&{>KBo^rBHYbKAb{U4B$uenvHku;*b~`8rq=T&wbiK9aa$?|_rM3B7*A(70^-jB0>ypOj$o%fbE_INP5wfW4LX zs-sN|$}c=a<>>5Gex{jX?#BheI7TQ~XDs@39ydA~M6^ny+Llf8g4*w6#0U5K3PFgU zc{ZG%I3yg0W|AHlU)d@>V(dV`}=3N>s1%y*}%vMQf(bf zcVI9Lk4L2%m~TZKGN!p*JLC3R_8a+k$Cwx^7_DrY`@L9pPhP~_exligu<85d5-GEx zch>cqnoo5XKsPhB}c%d#~Qci^6j>U~1=pIrXhB;p2l0NuLWdU z!tXTUojol{10^5N{||Yk_)4t9-IcwFjVi@w;VSD!2Of|AANJlmD#|VS0u@9+kf0z@ zB`86n#0JR%Dw0$rNfHnc2~AL9H$emxlqfl>2ujYGMnI4Zl4Fx|&KchM8oe`f=id3v z%$j-Yt@Y-=wwH9DKHoW2yK3*M(&37N!Rx8FzBYQ^S5sq#W{GJd6_27(kubt32?{iB z^>*-L(dAN<>9_FY8wc<^Mn<2sTNkDTyr;C4V-V%@`TdKko(FVw=aJ5y$dbif)d=zD zb`A&MTo0J16!!#JKv$;IZCgI>`t3c|mnSB|^gxbl?jcb1Ug1@{eGN={fNn_&=9WO= zp!9y>BV5R?=wf`9B<(1l;Y}{A)okrcsDbxHD2H<*^Z_6!V$h6D#l_MCN#MVG>o_i9 zdYJX`9rz&WtB%FlKza^^WYxLnTj}-&m*kFII^lLht2-1s<7564?0Tlu6zQ<19jxPp z`c>CKH$9u);tgMp?%}453fg(~R1Geef2nxC?w$d9;1l^di-(7 zcMId%p2Wf*2jl2!3CTQY=p}W#PDGR2zbCqCcj=b5n-s7nQiU8u?&N@CxIZuIM~vkG zf=3T4O&aX0&|TS1<*3H0|B(hIGP{N!4unHQR@zIwK7wic{sl9efvln6vdyR5DMPwU zz-T-O)pFcPx9N^44_(cer^y#PSU(pbnzw7Q0jgI*l{s+Pv=xS z!uzatdD&_Ajb1}gl1to0y|oY0-hMXy;TPehu1|g-G}o73L=I-9V-gvLo;V&1m@y88 zG!Tx#fb4JngvQViqG2P98O~)8=QAEZN6+0bG13r&Yaz6}-7xWWjBBgfD0{T`YPP`) zIefl?6b8kyusT~d{PA0ECR#4hRHv};-+7&Ilr|O0A;f9B2|f{IHu^pUtsRdz`dZXs z0tjpD-TU9Q3o(0!dzCda4zbj@thjcx{e5U684H=MGTG>KJ5r!uwkuudvUK{z0E-M{ zmerx5e*3X;uq!?5vi{jRTkrcp3i?U8YO&5h-sZb`WaidlNkI@1ZFj>2(&^;_7{eAQ zN0#K-?MtV11yEnjei@F|>-UC203Lj)NVar|+ZC~R^wjQqF_?Tw|N71Ob7#CHp=8W3 zm__%`_#!|dkd>geIWos8c)WKW6Xy#-e=YPX=CTw$99y)1^=7ps=w^(AQwwT0BRJ^; zFvA;ygHO3m$8Kgc`r>VuGAzVCJK6_{6(si+*Bg-6Ru)fQ)%XGGU%!t3x(gxwY!fdC zU1L98d%k**-EUda0Hi-x)HmlsEHWJL>FRRvB{mU7U2-+ZYK-OMD@8fA=Gly&9SVd{ zqa)>h?&Q1ZIXq*ofb49FRkX925pdqw$cPX7pXB2L<+c_b8W8zwLzE`5D=IT~BTOTM z0@AKbp!h>+fnft(VBUNGVXk@L0*TLdcLspqTPvgE4WTcM(;OYuW}D_5wyWP09f>V; zbo9T#I;Pv4-P)zTDq>JzMN`k%gTHzAiAR)+T>F>WhPLsv{gz~q1RmTUa}Ok~g)S$x z)(){d|GCT!bF&{GKAEFSuiksSAIo_!ngbV$9qeN2BNG*TAxx&OzD_|XF1is%wd)O( z68tbP*qQIxZrq!DwKwhufiPwQ66HCVY|BcP+u^SMCEL--1vRc`b^U2Y z?rrv(=mAZp);7gXeSQ7)eWkr0)NLR4jP&=`smnu|ln=y9QQIG4ZFc=z2r_QHExM=c zybyBT)}vrZIc8t7=h8af7_Z*iq$s0eB!8g4_-@kr!jMRN^x6Sn7XslI9jtmNN+-k} z`jPMTXb#(a$}9gJ3OeUCyu{dY}W)>DcsStsW5H4gbLBcWJpV@h0fiDA)1x#>oxYHRm zTu|pGrGAql!UWBK)N69y#x#R4N85~|VhCoNygyRM0uGDs>9NJYvMozG^ybb4Q?^T$ zUqPWgh#ub1ID&{^pF5+z_FfgAO7@WR&q!?9r3M*1QO8u?uxaG8<>3VyGBnh@rOifG z$U?}XIaO9a5GgC`xn7#OJ0N^=trP=&8u_TGaNTJj*F$7m1WHs^As1guyLtxKODY~0 z!g5vGkhDw`q%a*{7-*D%@M~5!c$OFd$N9wTGQy=y;NVr9dzkMj$%tbv*3TNR{0+&0 zjkSay0CSkS54-~-_;io7XpXaZoey7v3AB=h0+5}e@|aq#JdVXg+ton(#?vef+MKh}c0mDY+`{>Imn*yBW(gwB%DKZ1h%%#dJetFXJ0@E2@mHzB`w z&u$NW6O$!25Vbh9UJ=zd)$Ag~@q)xH&BZxHbfbd~i0}4GqL<5-yk)1~(g+Y}q6If8 zcG~0g>PMUoGSAwyN_l%((8AI)*dD~Oq@W*O{Z429A=PU8ECMk{y6QAf7rQI*y5`uwJ$mbO1@lp_k%62~yPl&w%*`1AWB|bnRoB-!b5)DSt zx1Mr$g075{LJ|n|pSYdqYdvwHc$E<(I4Bc4h(K-aH_O?|ppzny%M@~pxGWj}=y?Aq z=XsfLfXSQVM7kxEWAt~gfscH|&cfbvl}I7M4TUB2j0tK&dWC?4$rm#QIpnUQ78dfv zOrfE20DH!^YJQIx!NB+z`v%YsRW&Kaacz=!v#TV*qTlZME-%q9Pc|@#SaPmDA$+(^ zX504^jpMmeb?ZzB=ndIvPMpzSFa#8c7g(C2!a8RNXML;cZB+*CRH63#xoBzoAJE(;sHjWIsi3CQ!QU~Jau z9zw{2T##Sq4P|qN}^t%{S+I+*r3K($d*WI>Lh zZxLYVK#0zKBmB6`Z&qvGOOodpJMx^Z)J}tJhT4z;)WiuyQPTcJSXdz0KL&mmX^Be> zms$i9uv3la9zVwqXL>Z(dlRcnvpoAcPTL(2zNN*E4Uk@gNQNHZwH*AgoyZ<&^7(+9 z^(wo6OYk^UA4{ib{qFwbi;9V9TjA@{qS?1X?Uh*;ee+kH7xD^HwRegEKg-m$zq)EO zQ+kAzy*VfV@;b#DwjLD~MNxvLY~U7A@6_z}5O#&fLn$oHsuArQ^bpO#piF+o;{?uA z=%kMm30!DqIyS4TG8NYVJi!Gl3GNv#LU6j(NCl@@Oj~gcx_7RPyAeYvDq@4F+oZKo zz~O+0=UDQ}o%F7(Td7n?2@eIfbS$R?%P!=;2nP_`Me(W4**Qsoi`xOcB*9Mn9Y0ZeU=u}1i)ch5X_sl2LIK)w8DWZ%iSAViMM6d z6?`hN5~^GC2e{)#F2x%ExhD;xsR~Pk3Um=~kLiEIy#(#ZrDCFB`bWn*a_!PhbI`~D z6j8tL2kOKc;OlD6I+tUC)EQS71P9`>d{vDh?45(7c@f+AbdOX1JxO?1Mk=v;?m2q+ z?UX4Y7!HP0gFrwm7$El-oU~c?78A0369Oz>ChgBB^ZQ?p@w#t*ze!I#tiPe*lDd;g zz5;;JpEH>^2+;&vSHU5r=Iy3=YyzV;*i)qG)4*eCn-y**!om=XY0-@@r&!}*e*i|5 zu*%NQ*TBGtIr`W9zyIanH?HFTev?l0J-U#!MH>*dGh=|UOVlMAMxo&8Uy~2$WOx*M zd_0T~>SsxBvtzA#;F3*|Fa(dLF-WhLWV;}$~r(? zN!1zv=A~^Z8NIqQ71yn=qKit=M(^F3$<;I2>Fc%&tFOPa^IhVQ`Dn>=>hX^%!r3<& zgH|B2s3lQ8$ePdVMrSv0HBIOp^|6|Dqb`#)Msm3x^yB4uPYu{F?h^?wQBu~SE?%!y ze;BGB1NQ~f7o1o7zte?KhKj}U3Ci;=#x*SL4vnH1icHrl#NBt z*KvC7hmPLN4}tXbnc`yjMd!Apn4|&hh zkQcNU0B|_XjOPrtEEF;2+hl*ZpwjY~V0sQq+IAE`4C&F<`zhIB7NtH{5zVceO1S$vOJsR3hz7Ku9W$Ba8!A=9yjQr7aYA4bNzW}Ak3d= zh=%T2s&3gGwNjKQg3u+53(wehmPNTpE7oQ5OLS31)AYTu$K`%|-U}eZ9TcYfkU7pZ zOV;l*Tm4a=Wr_C!ZHmvd^VZB+UCK3oj85zKAQ^)nd|s1IK81jB?k+_M!(Gr(;ysm9 zBN0Au+|+mwtlRjD0ERvV^3Yl>01h|phIBFnsDT*DbmPL57r+$&VW=q?SK5-dM#a}h z-h;O9-5{#qIDn$EfA8i3qQ7&c?&8}I55v?Sg9HP}!?L{G$|Ib1-E2>{K^IYaBH^jp z43{lwD0K0Ta-ZgT4+xK-GheyvZ7$fzwGdumeUP!U@%4c4Q)=?IIBQV(NDYJjSwv#F zdW$$vl_q%>v`8GK4hXMoDyVNCghp8gH^-3RXHarPbNX}XzFQzcD@Z&U&eOL(*xi^* zXgMGDc<+TT$Zgkw95-=4oA};?-OafeF|FlWSYzJ@Ii2&&=r)xL#{?bqdu>OazyC?3 z(p+pjt}5K*&X;6)b_U zA7Y?uK#xy&Z}z8wqp$osRl!G0>ier~!XwT@B#s?i@p0nM^XH4_)F+^8=UW^{Mp4;~ zV?S?S&H@VT2%-*`$H{Dl8o%@|k|_&my7VTwIvj12`5J9TNA#i^tN(^eY)Yq_7q+NR z&6E^(FgzG$P&Z0^TuwqG^uX`>YWT9t_FL(RCRO<3y+5N8RpDKFr0GZ97Y|7KG2MjNoJMWTqJb_hyy z|M0o^2}&~EsS7Tn58yvOg29_t=A^!ofFtO#eU=@18xzLM_o-Jn{ple*Og>a^@E~al zELx}WBM$?6e3PBwIz zx-__eK)&>N%Sx-<-AQYqm_*aC2vnA>0^V@^t&jT#DLr=zosMnQ?(0WRnbfTn;@Y8c-49=ATzuqBTk6 za*RtJlFIGiv_t_{Po{o(xc%d7IG?_f!?b zT_}$+3`!9HIdTBlx4$CNvMF(bn2$X~TkK;o{B0?>(h3p@gHzybaQw_C9?R{qX@Fj! zf0P_BWs8@XYmBS&q+G`rCMvRXOVuf14(c72sv3U<1TM#k{&|Ne#IWdw zRu&Ip-`ynehVHu2Zb$FP2HDXDZG?dH&5j39x+!7l{K+a**4WK@UDYNmt{mrWxzYBiC$Q)Mk-a^wYW@Pejmg`lIxKbQxMS}YD?H(Wntg{e zOl>9~AEq1a4{-HCrCK08MnQ&QUmTW}CLh%VxrDDda@qX|`y!uZF(Cm=CE zWBl*=%2uz^YdoK^fQ=&hDrX%AcF|7dLi~k68ps3@anXXBmg6{^Wmq)S&CMIc*`m+b zn5Dqm7&jT}<~++Gg@wF$g`WyyM$Wvqb<#Y}F~x&p1k#2X$>=th`?Xgc2ocMI+n+Oh zxq*W_+D^@!t9K~_oUQ~x24$lb2gut7b?Z9I-h5lc^Ym$t;P2VV{@=5c_KSsCWJYnw z%@9a&MhVib7wo4Qyc=2>s;cH@_*^*UV|S6``@|Qo*IO9cm98AVmt8Xg@R#xX#Ifos zLix%yTRRWz~U*qMsb3J@Mn2E5FkH$skSg z#V(G_n}yr9lIS(v4Jc#t+M9@w4HB=B7oH^(JHCHq-=#cFNm*Y2$9Q%*oB#_m@Lfwd z;&tZxKTMQ3L=sGw#3Ey(+n;~$AHpLpz7N_KlQ2dcBp*Ezo(vzWMA^0+$=NmjG%~e) zd1jAm^G;?ji+_=p2jk8r_5Tgpa2G!k(jKSCTx^s_}~QfN&n;r)eB%LY|yS>3~?E`Y`pt917tcQ zcq}Y?Hh$uWk&RA|^pMN`+kWOlD4ph#;iP*iRr$Z?HOF{mcjUvwF9$CIMZu2wR5^FE z=vY$QLZy6ViJ8~o9W-()rcZjd3_?1&PkI^q>Bv`ru#3xiX{@>~jPrEU!?o4aj;QXH zWnXF9L+8FPUkXm09DpqJz~+f054R}r+kx=Dx5pzx6P2$9*tz_kY;K%3(3JFc&enjC zh@%Is%k5oq2;M4Qf2yH;66|Jtg@4a9H-HxT6r2z1C!3Jy%_4K$>|2xB&t? zK~J}}U%R?iSEulo^rebL6wD%2F)HYQIxQgQK2)5*V6Czq&40ete!BIZC2h{f?R}u! z3#wCD^vgX$&Da7>#?>&N7V(hSvSOp}Z~l(xG}oIP&83TB5G&TBKr6+qm}#zdELF|@ zG6Ty_9pSVSK$lRbQ0FpaQIJvBQAhG}OUm^c*O~yl8Q4fUvYqLVu6Kq&%ep4vA$Z{- z#ZH$yp0`DX)}lwjAm#3-E zu|!j1a{+On_VSJ57D;q~J6@i=lT~x3^~W~@h19GKDXYX z3AM13jGaOgn^tk40add7LJd zN|qQvVSf(inKb1+@?6)`eOzdwtMRr2(gub6eZeD znT_}sI)DIHEuH}R1VisrWc2g^?82n!n|>0VwjEY>_~8J3J@VsZhu+3G%tjgQKjH2a zPpxCcyCCtv%#T>oY^R?>5srd*eB@|M@Tcc3cZQS^Tj68l$%h_QD@&zo`DZQ)UK4>87TUKx>oxT6%0}+l0Y`dGb7yj{_fSyP zS&7QkpWm!2b+&0m??KtfVYIoPmGw6exC0zhnctbBUiTa}-^0Q;9fsU(c+C=h0WJQ| z(w()==MZZ*Lz3MA(oiKH$t1M`(=~@*UA~Vvv+6}IlHvXl3X}?dNBP~C!bD2D4_@MV zzm%*42JYPjVcHw?=YQ?oi9gvnzq6G01Jdd;Ormsnx zPrJ7P_JNfXZs#HS5Q+^Tr%Dz7$4(UprcT}l)bXA+np;aok5;?dB(8qgY`3BK`P@@` zxwXkAqxm#@x&Hix*>xit3W@yNU}AerQPU23%LmJ5e}!c-x3ly#!I-x{6&pymrSx9V zEr6H%zqurwK6}RK`R)s^cu#V(VXJ zK(p71mIsAD^810P&~%;9#LLYB2<0qD5?ozZz1H`x)nh|P?(?o9dk;8s^n z1dX4?f1{AlZFFx~OH`Ut9sD#dn<)iSs~BK8ioFLU=o~%v#M#DQLavSAG6%b53Zn0XUT4ebn9>R&i7Q9~Xrq}g3Zy2v zL#r9SWxgJEd;iXCDg2Kvc>6!RwD>%Kw{&q`kQ;(9qpA3mg%J z)~2UqBtY1vo3XSeEO4tT61g}G!j`*5G-nDSZ2s-DvdlE9zNCW-KoFE-4SXtRH&EKzff>MFZE}02~-dLO{A4 z_RCPv{IQ|T_&GEy$_oK9|RwVJ4itQtqmePy_56rUZQ z$(=n)xyOBC;jwGMGKt^Sfm6jK8*6DBZ<7DV5AuizbM-xn zpv*ZuM`ibPfK1|$x5f!9G7JRucwPHNmkia(fhYz-C-EIZE&97`;I@6ay;nlw=n;qN z{Le}dSc+%dzw7Cmh!N3=2g-C=ix83aT*V5ijbSu6f%5>$Lyi*}OVgr^clsXc01b^n z1B&!EA5gJ3p>4<#oi~8ZeAX8uf}k6&gu%yh2o&@06Soh2uHgL_P;oR= z_e?hU4tw|N>($&WD!Bxr+3QPLpkIa)^y9FYc3tDQ>+dqwC zA?T?62mTOa99fF5LR=9G&0UU30j!|g8H2voq^#kd`-T z(Ep!XkOK|jbopA0;iv9TNW1zuzml4IpYsiuzsEdlySiGX;>rO@sOGcfZFjoM1?&6r zmTFJtBsS5qAFOgS)b2YqN70Vf(IuhaG+gv*bC=DEuve zt|5is@XNLIR>WUm_5nN1c#+Y{57H2p6bg>#Mi2vEjvk>!yN1$gsrB zGoVlqL=aw=Y#D~3G!zC+32;%g;t4#Du#Lzp>M%=pu=7dPh&;SrWKaZ7y8-nlYw=@Q zVLv}bD+%rO8>oW?X(PbvtU2O;}XIm;OP$L%K zzB{wLRJjS%lXW2nPIbl4!$R~11~50td@iF9{s(VWX4@IC`ahtIh4ji z?vi;ZWN!|KSFJu50rF5%VCPl7} zShEs(*o!Vm+!KoZgZ*f_+M$E>Wcjk-uKe*Ob~Pkupa&f5s+Yku4<+^?WH=#|eQ(TQ zLB%WMw6jH;oJE8mB~G6Ychnw~M=A^1mN&#Y*zRYM+uPi$4dv8I*kcm$8Cmn(!Vsa3 z5je7|umPbf94}fL7dmtNqn+waM;6vvB-rhhO2yjGyOd8P_9xg*ovfTYMQGCS(U;O_ z&jZ#@LgQeepp}A#33;CvcQYBO~Z5J%T;~^OBrRB@u`|?cA(Ofm7P6!Z%ON~`OcwM zaP`S61<2{vrQ(^v35w(|1O;EbkoTq&H!8K56hSmu<&`A^tlSiSzz_g6Nuw#7?1}(b zdG*%)q#uxHy8ol{dL-n{6c4|t9UVuqOL`poXF<1BkcY#O?Q(N-g`6pn?#bj9Y77(U zk;UehCxQ=EE?Q^77`wJxI-P!|9Di3oxjHHMi`Crzt$MQPzg#^j@Jt%goDx;zAxS^W zxn0R-CQ1jD0??GLuilrb;ZZxpdVJaz8KCCGoX^-5KLMJmelw%!x$!D)!G2(eDF(Jb z$j$U)hG2WJL{_K7PuNZ=uYm))*#^L_NCgVe57Yg!Oq@`h_pmI{7m=c@-p3A7l@C8y z&L2zT8F@gG=q)WQbjOyCNb{8JY#d_s^@ZiPT&nO_L#jaf@5O=Kqy}=0&iSY@vm0f< zl4hV9y{J^#KT@Z6?iaVY2e?gE5^;X+(naS9Ao?yOtD0Nm-q#%8m74k;vl(xNSZ;aY z=v!6L!0o)^NG&naW$xVM3yM9|RX>-1yj?z|S~gKda-^A4f;NL#p$wSNw3{R+_Laaq zH!DIc{Fle4D1yjtJ95Iz>^ZQ(zMOkEKr*q_i{H8BZB8((aKt-7`8|Xu5Bz3Ld&--#Gzs zchS})joPfBe0UW!!mRJ?UT>L6am)dwU?UcH-$=~Fid4=;-F)+ft>Y;10jLr(IQ>QMc7#S+bz3+H(}B{i8s=r(iB!FJwl=#JgL0zZ?FzYo1!zmKO%9s9vm>89Z_j zu^YJUxHCb?r$gkD1eK@Zo_wi<-$4(&_(|L_;Kd2OtGYROyeuif}+bG}boapwYr zPvG1K?apq$Z@(bI>*!%fEH^EL$@{ZvXxuyx4oQ6dd#bsM$H{^+LMBY{NiFTUidsg} zWxt=XA3g;X`=#l&%c44y*cU=TK?dhNnB8%uP9RiuYFNG}HZzp5+q9@9KExP@*v~>t zuya;bZrprJ<9PvQuEB!b)8hgRdEf+1X_2^t#{ob;6O)16CjDl5jZu5A_)Q07&>BTI zK+OrqJZjFbt}Wl9B$KuL`zI0fIX!1xE7#w1n+*R9x2cwdQ4-JLoFXFq>eFHYq1h@c&gT0iV$Pmi1uD48ougpQVzhX*l!2O~IIg!l#EXkofPjlAl8^xpx9 zY8f+(L$mE6n}BV37y6mDB6q$1ZqSGy+ClN=f41l++heshrYu|kk%5P zXp3`$@P<6yb5)UDHAyteNZyQRk%E*nXbYz#9gwGUxL_Qxdrvp=-rH@ zoS2vZjXVc3XI5A!8UVtqnoGgcVZU{05U z`opf)>lLq4M?72dD`+$kIYL@bQcHwbQtF;V>+r3suP&A>bzC~S+gUPAV$Rb3VHS<(4YGPGz}>>;#Mw_+Skom)vI)CIzkMvS;j8gjL~hj~GiY!R$WF=d_@*0q9=lTnz~cdA=R9`H?T-)>cF1<}a;)19C-H zY+{`*?!2x)jqS(M%|7<$leqBmxa&LanPSrN z=%IJ`jRpgsR&bq;3{LyfgZ}laURPMNadGyc>@Fq+%d{5J0sI4B7DCFbvVh*aSsA2O znnfzUVqu!mN@^@1>Og(Hz1W_p@XR-rWHe5>BJP=D-t1b@wZt7z=oBFDtP}2eHwXU^ z6`?XgqAVV`(4ehAW$!;YQq( zrm_Z;ylBg1e61AGid@Wrj&!l^Ux49?V6(T@z{k`M4p*Jc8Ts>UXpT$mvOtNvsk;3{ zSUPpS)IC=qHsIi0xTMgsQ%Bo2#;%kce4M<#K1P(nkjV#<*CLl?BygVnieAo6j7||P z3yhfVnE{u(_fFmw8*{dh9jXhEZ~Ub;teu6GBV{I=`&VkV&XhuZl6Kb@Dba^U)n+yg zwY0oCP=f|OlSUHXP8b}0MSXoCVy!%Gdy(gRo$3Le_t=o*lm+#5^G9};R@O@iGuhuS zrKR|66*Rnx@~W$c>9uSYn3g3e9P^hPh%w~y7JLk#Xx1@-BtA5Uf3~eTlqvO+Frg*v8w3mAc*}{RAs8{w5;)7lb$FxyWlXAR95`}FIxB2^ei;z= zDN3VTN$q!mu<8lex*yt4Jl&wV9*|m=F<*PX6br>HYT>E~KwO;7j521J;3xm$~-)Zt1?dBf?UJr|9a=+E~)225BO!UH7^c0Mg%Uu*ew zjnS3^g{b7a$kUEe`d-qdZ_q{P z5{plh3U5(Ts-2%Mz~4LkR|(AH!p(jAlTf7JO-Y+89WMw1pFWvF?-Mu}=7i){W6kwH zMI4R06%=|zo>uuik*}yIUEf<&Ds|;5gA5#sL;>eZrM{ zLsH}y&e!5{L7(_#>V6;ou*Z|oU$mb;Z8LGcyq==umV1=whNa`~Ow#N#0XSN_2JIA< zYtf&sq?f4w(-%hcM=v2WsDMn-)j&zBDJuS&%l51HkS&aC8hH-I;?sd+365*o*O5}v zcSR*OHv?De*PvXSQB|<=-mD#6Ovg@CR;3WY59dyTCkQEGgC6Y>*!eY41IJ$ zb8nGGv%pd(QFv+TZ^}6llu%Q&U4)^hH7Wh;E4bjMLuHNbB)2|@#%a?cf(>Wd?tlx= zgpNLiZ3=2r6RLsYIJmST6>h%Lc^BWrH|_N+>lMmA=K05pdmxE8zvo6K8P5O(wlu3= zUWM&=U`#L>=;$_igB>ULxS*4lEbAYb-D%(z&RY>%+<~HPV$aLBtfNoJC5#b|I*?{U zlNpx*m>qucr`b+TiJsAv_oU4fT4ZO(M!9npd+H*8la@(JW3u++L={Vxd@cQ6h=5Sk z`4*+X^YZOSdp)_!T~5F!r^SA^Jf@jJJuMBho(7!x$kD1%-;?vvBMJnOz;BlXr z45GtK8IZs9tilT-J7cF4$y<|-PSR2a*UEUjtBz)nJU8=(T?ij18WKuwTYWRH{c|=g zmF_PaI+Al6qtXTUg#3ITJrE0kv!dweA%f;Qyux+9@;sb~z3?_LFR~d{%=kT!$^e1^ zrE`Do%x*(fkgqK3mca!a0!VzeZIv**Tx-UNSgU+jECupX&y_~L zl3Ibv^kFx$`z|;DsSxK@tE(_*FXMLQx)yB{F68n}?Ee5y{6kSI0hT&MQm?(_%xC6c zgkQt3MBqT*dryRYEU`2myYLr)XlAQ-EEskDZQFnglR4>AT?!K0GkMND#Mq}W2}_~- zRu1C=XHky&cLwQ!YVdlJ)gyc%$rBb7RA3dq!7@E7z81E|X7t3rvtkOYo7bYl=g27=kcf0n9$)J2lNeIKgGmdj{10ZoYh z#beJ&q?c3j&hGistvd}Rla`RWU61e59Xr6-*4?G;e+4a95r@*1>B);^;C}YqOyhW2 z+$47~I7ztPmg7v(34S;Lqx!KkE%TGSLCi+UK zbhtirYEu9Pq6s}VPXnc^17Qj5#ei#_$l#9)J$@gss=xtZx-2wRW5BOYSt7vDE{UHL zQ>T2OZA+68f$H9m`P51#n!883jcP%Ll@yf0&&cd1a}vGiuC0xHr2;SrQfCYoh0d)d zEDcBT--C65f+5R*NpjNLlu$qRLz|Ucz)Y7J0AQu}u-Cwh(Ezm4#t^uYXi`~ELHt5Y zAy#Qc3{~wE(2+rP59feE4vMd(0V}2dEMG55*0^xLIK+d}GEHOlW?CS~wc{)(yeV9_ zkq6M?*+wKlDBBSqBC})Jk1g`TZx*>Eu!CakK~Sy1^a3K?7h&)6lV*4A(lCgoe z>H(&vuzazT2 zHz)|;gwj#nIA2E(=XXN){`w~b!@~RDud|ojACJ**IFjvGIPO7NNN>sJ=GmCs&3#+%n zQ|^)%u*qp^-dAgcLFakH3wr*hM#nfPC;N;Z*=FCv;1*k%?TAXX^(=0j)yn}t(Dz+q zrO^R#uG-WS2Iye|+U7tGa1t?%T!WYa9m4-(cEda?O% zEBd!WM3BdN)or4`n|=yxWFjOo+Y_>$8b9>IVB#G zba=Y2oDE zGEXnhAo=c6h569pyA3!@tJzPJ!B3Q(SKI6Vb{?U4H?N#oLZxR*s!#bV-P_2Gc+LeT z_%-Op+w)8*_TB8V^#DO6t;;Yu>1TfcVUUk~;04wB+riyB70|7De(>(GFXq-_(W9-Nigf_D8E1%)ygfbI9^43QzdXOJ7kz zBaUFSBRfMCdLIlOza5sMgGhg0HskXip`0Q`%UV+lqamQh8>%dv5NQbAT&;M9x@4e@ z#4)e7L{42zj44fxjD;xBlmm-V0u=M-K0TGH7V^!jd&n1{@%rOW@~-@mBU-Z(=2zgi z)n!V;*Aji{lbXWZyh=f{_tm$!HAN6Ci&lqFN1(6cNstLFc2d+r2<$7|Q`BrZf6OG? zfrOw(*)P_z>1X*Sr_@>G@_En=cE7O1D8p`{YVWlD05ebsaFsSs(Lo0tq?ktivO>nc ziJ6Fx=_s_V2^YNP!TexF8KsR#M4@_9WfGWhp@V+5QslLn$nLc&md0#R8zx6nQ>5`G z5Z7`i_J=HrykvY%tfmO63`k`GsB>AK9g5q|NT`G#vrRjqNqrJ>dKH4EAByJQmlcTy zp>nFjN*PR*BXwG;ry@0dH3uoM#C>yRQH4mI-PBYe`S7yiB+4FJPywBmHps+f#XgojrS$>Xy&`mpp=Lu=098Im}Stinst@N)75^JQlDIh{UY9Oy+`H;Mc-)YZxy8sUiWG?%T{jpR{q3wg?9u-X$bpB#5|vu z$4s~Q8ZgrzGBS}mUAVag*W!w=6Ixhx^8J{)0kGr?C+frZ19<1avxZ4>Ny6mwSZN(KfE+Rd>CV<$C&$jsmg^ z6b3u4@q!l7Q14c%qM3z)K_52!pzASO~l914?Tk5W@k`@o*$#L{G<}Z*y?_jCg~_fdi%88YBM!0is4rDdAk|MW zd?OoNKS*>*b{3p&=Fe&wr-zN@;;nc2FiVX>LbfuRO)6!JF=o&qjOUCUnL#E|SR&^xv@w5^`P;&{k?Q zixX(tO5Z=BJRkX%5paeLct?}l=Sn7faM`O0_z_Ku_xn2QXoIE}Y?`%ZNtA+<*s+Iz zy%G;+W=YvBc+I|wOZFh88^kVJY-6)D)T_>0Vwq|XZC9RXs?1AC4=$8zPA|c zCBht3SP(LP#d@>PEFeLhYy_|IDTQevqg|6zoe|2IlY3z{9q_KUO{qg8b1Zrw7;{l{ zdL9;MhPW1!TVS7RBJV6<>OtMotJxV6&5?;#n~$XT-&9lSOdt~NPCkeoKi?N{rK#Ag zchpy~W5ct7Q?$F$2@lnY_~G?;-?Oh^4kX4>{S!SCCW2&_3bq`H9X+UBBOji(xF!pF zSuUqdf}4_@)@gg(@%Tdww$oZq$ur@*H_y;rv?M2Q!INFTr#Cb`AZv*-{oX$VnxbDV zMhrE8p!IDkNzQp<5taNtsll}4+)jt6}L(V&$5&8MZ4L-yOEtq*u8m4hLrlU_A zO=VF#1owRN63)=E8g;UNU+DaG0+eS^1N;(VZR|lUYY)W@&LiRFrZ`oG%OhIA6it2= zlu+{YdjWqxz8Zx$1pa`RAm8(DgDWv&%|x~K_BQx~a^cJ-f0g$ihqYLX_9rV~;1Axs zumIl>cXDW7^E~f46!-%;u{rpLY1VCuJ6z?C%T4}dA6GA9(zZLYcPc-EW`gB4_~fn;P}^gd>ctjlaT((;N)-Y5mm)ek{M0=RiAb*NmMv z;Wr;^^^u@J{dVErdRmC;THmUIab-D-je!3i{UZ|;Qygk+*O*Y4;3B|(hf(COI8^`K`Kd@(-)~%J&x29v|#J?{x+h?qHle=;b?l z0(89vv&oLLACk1MumINg0goSo>>zbPf1G4{f=TV1z{+)}0!8p2K;81a2TwpI5C~0w z`+%#Z7Hf%3*{1D@m6W%iFhk&hh5XJ+CzlhMK-Mh%eNQ8l*cX|WqBi3S6Ft=7@fhPkAHQdFTS=noWPw5KtZZZDgrE7@U61V zH4pH=fpJFP+^X;5vw|PMY9p9TMD_1Hohvz&t4_GVc#YT?p177$< zzx_h`N(u}sz+#t`#;{Fy3NikDtVZb_adm~B1g6%EuXf6D?H@V|k+_1~Ck9Pp>X`lgrp)Nas2BdfT0 zYNRFcz_PsKvd&>!gX#h6*F2Z7uY+Y>-+vQA`U*60e+Nhc=)er?-W_=x#g*!pc&~b%6E~`5%26m|orrer@ZBN$N{UZ6JKoHhAOhMhw)j z#1e)~IHRC_#^^aiQJiNW5&UeMnw^0ZbMyb;2e$EmX%tYvc#M3(*v6~e+$1o>TpDa* zg+^b=(^&j5zmoRN zB5eh5yLHTr!xO|uPoytAeBeTMamH*1?PQZ5nw7Qg3h}yeSMB z;=zmf@7)>%?m}R}Lb+6gSYlu)VyK$?_}@Uj`P=)D*evLp!kHcdB*D4)v(l(O);T{I zG<@}rf`e*ZL30gm!|!jQc>qe|lA^3XB`_d+86(ewkNp~a?SJ;I=>f*$MX^G|oWurl zRg#+9a^+MsH@faXEq_1YjYtKuDp7d2!Jv)Re$7C78+6hS_|Lv&LUAnPsla4+YhV_j z=-9k3w{Bb992lbnU+{%ouGSCn-$Bomt!@6K`!mq0wQYi1iO~H%cy&iM5(%cif#+4c zUv0|pQ-+^Ig3|{2m){O67vjXZ175M7H{EoXECy$6W|cC@Q}PRV^@&lZ4++6}RPNcW zr9oD(3o(jd(jc&K*rdRqLDuk}ei2;u-S;_n?#_DLmPwO~iM`2I$xuw^q)0}~ z*hoo^ZHuj)xHw=81{E{H)o%* zxhS7JGD+nVNH!Py8z&A{47qiR-*kM|z2$SBo2y8vi4*jbyNx<>i7H*4C0zZH9Kr)b z>`bWFBDWp{8dRqm*-nI(AMUEy?dM8^(HJKkN2*N>xy+QKc^&h|o1aG_)|#HqbSC+V zUEkKsu)JLKVLs%3yd;D0OhWK@_h5N#ao$kvOF3n&^E$4lrU>b3gP42=;HRd-*wuA? z&U12sN%vvW-o%lHv|`5L^8*{U+l+`?5cwFwrW)FBS)|e|Uh*m$VME*trqr5e={iO5 z*kkX>vssSSKa+dIJQS=3GdAco~Dg}+1GwRk?ak;k$R?z<2hj=GWU=w3;w=k}O&Q=?90zv1dVz?bwqnVEO z*Zk-E@>IIgj&3q5Q`4?-wO%lj72YPPO2~D!$|(9g}z8KlMpD&OJy#UX4%PKQ^Wf>2=QVxxx)FyXFfA zH>*`@vCo@ON~dRYEK{+xpYJLxjW3`NQgWBJ9h`D_E@J5sBoq&jvg@_l$ay%u{p7H6 zE{FxLYwKdSaOR!7aZ@bY-qw=8WAnMRqc=tOnHh)4u(%yPLN}I*=X8tW8)Xw4GoJgF zALMJHot(2ok1F}BPuCPDwVrnFexB0o6HMYo$vAHL3Gd7Z~FP`9&#V3W1`Zxd4Au1_w*za+{Prb{xXurqbGr-vt z%hyDZ;QGTN=n1oS(&lC6&x00Td2?O2m&A2s;nzQYYF84`VOBmvKANZ$MGxn45fe&T zY*#t()~wi>I`v%`Z*%@YG4@F}@9fFXV75b)w_3B&y#64ISpKr4w)6EkeEI<9`19@d z8tzk;a_m9Q8#5fVf~odV(eDtW%_56Dr)~~PR@LOCnH}2aa1y?iD$h1y- zg&0{ZS>)RZu@#?MmgnYvSF~n){T2_n?=$$1y-M{Lqo+J7IDQb@XQ-l|w57V_*1Z9@ zbmx5XP3qprB=j~!F{iDU7 z$41-Ke_7p_+it#{?Q&&})r`Qy(mQ>8@CrYe z3!84qn53+vo2O4bKB6m^?`L^fk1lg&OmqgPPJ`hx>2d`4DeqG|pXhzx9Q;@V`Z%n7 zd^Yz(Y8}m}=3}Qkva!23kd^f0GPb4M7%eW*;{Vg$cZEf{Eo}l4B^y9N36ccFR-)t# zlD8rtsWFhF@4ELLzLBD6gZAF1I?5R(7oUymdI^Ye=0Zj0yM^u+5fJVW_o|*aU+bYD z?&UEvpfvLLr9D5v=7>Sa5BqIb*gRcoq=e9r4n=3(F-jCLV=6m`d-CV=@U;f#+T!6c z*;Dg`3VSv2Nu$#7vbf4_Ve9+#Q-0Iu_9wQ328#?Qqe{+Nn_M=gf~RhQc6YJc8t(-s z-H&;Oac9Ta7#n3=r_fgNX5UN9&KLCrzUT9O1d~95*}lhG*~XP*a$C80_?CzbrJh5k zHMXYlfnt$G64xdbuQcPz1`^`{R(v%qH+gC3!mWcOSmWj=W$^f>wwfBdmws)&Qd$0bz2K_A%`xSrEC zG*@~TE@s`skcv=FHZb#TquM4TBz{H-ELJ|m(Xz*{``Th@{aTsPmx+0wUXZg)lQ0`x zRk=aOJH_Q>J&ed7df6mknU6c$O%zD8SgmoIZRc-S`=DS&wr<$`mX)hx(wpC9rllFb z1F+)@?qn%JL9}?^+E&9ozC|j1Ibwp}d^X@)ctN_N3R_W7l2t6zV9A^({BeI?54p_e zOv(B71dZEuh~nAK*nucCTLpL5a>~;xS#S98(-vvzF3nWyUB^NJgiSw#xoUL!6_Ybe z1I%C&p8^a2(gspC>JgG)wdA9gu7K@sfY0pvY%&KxDl`Afocq`pp7jwjkE9FCIr z@uwIQ$Dxeb`voRH^G^pRxU}7Ku9XPy{vIQHr9gQ~k1-qY<)yY0|G8 zV=Gmx@#v>DaC$*W*xK=n8F|NgXI7QIU^ct=f~h}gsxqI6seHGTP;WK{DrL zlV-cr3H-Nit%~d}iS*hUE)i|JpT0EQYK#nK6TJSx>4|h-Wjw{BzS|wC71mE6mNFSm zcXwCO@*RG%0tA&?j zGN-K@7oSZ@^orQ_h7l4sl2~e*Jz=?G!;K6X`PBN?&Lw>C>SrQD5m;YLT|VU_+5@aQy<&8qrzk zmI9*YjZz2YnrfYRzRtb}S?5g`lL{Jv~ONEfVd!M5m$&BLCU~HS^4Sq~~J(fe2 z2Kzmk!ln7M%5jaG|3gv#G<_ah28lf$jC3d(>$}%!rPcHlfRZZc`4YP|>6KMzpue)s zNDlmP*+bU6r-g?o;BsrHw_;t*TP0ugAQ(WN;pW@AfprR+QTeI`j@lioVTEmbotdbx z=-w~i5*oCuO>#)DbBGqr{+U2iT#A>1Cn3nerl$1 zVSzJ9CL(#b2oV{hcF{7G?UI8Qu8juHA-bV7`tD<>WduT14-Bx?SVJQjWBO zHHph=jh_6EPtkxC2Em5GS-sicT+Kp=JgLmA{oLYn>NQ54QIG(s>+!=78A7< ziS537fiUYla5nF6{e?@=S&MZFDr>54lE7$*W zG%oD{WtJIRkCY@@T>PFrS>xJBxpAtfj@=#eUR{p7PZA!}UH5Wu-;cuAbNz0FFtP;r_-s!6ze-|%DDmIgMQgf;es zUzPBRGCW%mADB%rFpK)J_g<|W-uzu#$eEETP+ox`I7rU2n|X&^60vT(J7_#1%Fmh4HnRJQ0JQu!`0QyMvZEA!`-6kmT&w2 z2dgd8gR|?wh-;Wq1l^Ihpk+6oo$F3CtO)gVSQ^*Hy|_P_Lldajo;lHJ>;EBc2=@{d zp?4O`q(3*n?)z|Kd(1VmJw`saYrjG9r~^!Hyp%4w{Wx*}7gj;qRKvAk{F7GN&+G+* zFH%2GnKBIisIW}fuD|`(_E@9$){3hQZRtz?X9R>#nYs^gCa349#5N=!$)<3x-ZypLA%+3WO@ zN#L@3S_J`~uvb+JGZzVgT04c^sh6YB6owje^_<|51+qv z;~LH)!b`si~@9JEVr?ubrQzt_d?YiSPS&F1H~mM-VJbSlV+V zL!fSXJ9tBGK;t$_v&Mb(x#&I1l^3wltvMTJPpT1lAxy>L;@x1DLbo!R$=>V)zdO#8UWcP)JQdvg4;yXsz6js*5Oh_g`O%BF z7|Rok;Ax4eBuAw~l`!+E;EQijQF33ch*HFzdUb7DWUrTP<@-F?;>yZ!p2Q*S0N-m?_)qGM{GLbqKy%S~qOHE;7wK3=G{xv20ndX+LRwlk!)7c3hvx*{w$4 zf8j1%ufh-U)+BKJt!4gaB9&FK4~>&$mHgT6Bq3ot6-1^oqS$^}T-LX5n?l7U-7cw) z6yp<9!-p&3-aZkStRXe^FqdRW$v%dj{6O=`N^+aau;HOa&%|V&O*yVaHA^;- z@T!n)TnfXfz6CJFBcFG}Ot0#6KO1FNEkx|uRM~oOOc7#3v*a=_I6#{@f`aZ_wlQO> zus7z~O&y@UOX&LWG5`G&m&&1uRtV`H$bmuJO4N|j#BpgtH8YN;u>mx=bAKw25P3+ZYezuF7SuX`2?GDEhQk!{kqbrLgC zLh!J!0SQTF-cY_rU-Y*CPa=D|5mNm4Fw^hTPw4?yNIe-qv6naEKzmT4n&!+p$qvv0 zRl&f~zavAkYDwOJN6F6b2Q+$gTfG$IzJoB8YnM2FGOz_@65zfB-fByaCBT{6zS&t8 zmlLK_X2n>;wEFtLZ4SU2AF~|pqD}jAEOMvpy}LX4LAPC@hAm9{F&Kb(3oBNVg!JA@ zc8n15g2q>-=Lr1yC7-^jS$(=u`9{sv0mny(GBw@eVS|!-R1U(vZ@6=T>Sw80Y3!Ku zk+h{B;&#H(4%5wi4YMcnhP94wcUFc*ex*tM#5)dZ{_OKd>s9Ktd(rt?-Lj6k_*Dg3b;A4k|h_s+fun;-+YqunKgx?rk;4@0jas|W~6MPKz2+}GZ5Vu-$ z!#*9P{A~G@iHQ8##f?<4M_+JE-bi=ff%A2weULz_!rhlE+Puyahb|Q&TIc#{SWh)1 z-0V5C2YPkmFc%GKRvYfB2^ZB)6|u3z0joc2&Qb$#HsJ`z zfldceo8iIb*zIQHPQ>O#_^@S?Ktj9NqzXYDjDh1X3&VT6yw&`pKUCT&_m~ioo~z5I z1N@~Zw(TPx)gVJUiUPkUYBCSdJezj)`N6C35CD7dW0;gL-5{Fmb!lT9@5zE9vUC^AqZ zuT%QnLQ=@HW2M|m)-a6rI>#YAt!*PueitJ!z2!M^KFb^>S$omjb<68~E7 zT(`CUZg<(Mc&rUBGByyy^3id@*JFO}3BF14YaBW8NpjR_MlNY^KAC${HkYS(jTH1```hAOZqX=Ms(C*Jbs z5IwI;FDDllD|U(R(c>^Grt2=$7zDn+A%~V=Tnk4 zF*2Lh2R}c$iBteJGykR&;3fV1Um+VNQ`pewH0)^Wi2LZQt^CSFu=b~DitQq0RS<8C= zrw6MDil>T-q;KO~djz-xjT%53)&i0YJJ?`TSQJI&+NH|m!=er<*1I=nW$Aro{J)HS zR`lGiP%IGrICQhh=JU(pH;fSWb7aIuwE=t8-dkghhkJYqMtS`Vx4`&TyBc9W*M`s> zr)bFmtE@o#6Du@Y{LNEhx=+L_w?hz@&pC9l8I@7K!|9ik){M5<$@GQ-90aY&*s{?v2hHS8CEA!CoS+vsYlU?JCjPC!UJMuMED&euqay`2F9<7E0z6ro)) zYOx>n+zyl$%)Uvr@rii})GMuA0VHE}5E|Kl6-kLjHpu8jjPaZA1bmxd0BB0>E4KW8 z#q!|i@b`7c?SI-Ed7#%i7rJP7?3dCX@WXcwLrK5X<1mqec6-7L=IE!?ZvGgbFx#pd zFl?rfXBZZ_x49%Eg(-^lrYRJ}pUIXr|RVm3UFo;WEAFhbQ~3R=Mnw z-HgP3+#nihff4qngI>pxTa>-RlsBDWmmZGQvFqPM@Htz^PN)2K}tC=xdy;0SEHrph082Ct??J=Oq&aW)I2cDMf_hKkZ839Uv(`-M+9j>h$9>zgHCV?O^#sSeyU{+U z2r|dj2xDErW!bJb2O&W^_*2{dag)S>#fkO8j-{*ZOc2ZfblGT6Na9e9zTag;i)TxU zL7mgt|7`N?q-RjqIw*53dM_+t!e6tMW}Ac%99sb=a#^^CCL{IB(2kAqb)vRbW+pLP zN0E2VwfuAMuFED*%ps~Yt_L6;B4ZB!C*^N$br|=QkZ6%2yJsVs`zr9MSybN|(g>$X*9tSV|8`9Q+c#wwa707F;}+BTRpM^PgLS<}7jr!s5TlQ0!b$xb!2HIQx~ zWzpC5);}#e@6s)<41X?dTk{x-Iu`K8u73Z>@0}4iU(A~`j=&@7M^AFxGcuo=7BC?{ z!V9YN$EsaB-mAHGa#~S3Lboa4f24}cT-_$%M}|{WoxB!%!sWR(P|WDY-OgK(ZmVCu z_|~A<@exlgJkA+ds4|<-JhfwZk$|Xm2X$)VEmLzSgIiiO{=et|_8BZ3ms>;eztGu= z+a+2xNz#9Qw8z4kubyz1nOW2?+Ye!@93un5QR!UL6wTcr0wQ^Qu@G%1nCl&q zuKMy072A{|z9O1n*IgX?QTe`u`)9*#QhfD;?t9!qQXTQj zy=jnC-)!5-QKn(x%d${l+V*iq_`Y(;+IT-`MSXjwMN3TH#dmKd@#8Q`{379wHgy>N z3v-tzl5>uSy~Kz(^3!j6NDJL(^>ksgyXV+hyvJ57il(@oF0 z%X9F=Oj`9iQjrzqPAi}NOKnIL1ax7!b+)bVwz$}TnXArX_>4O{(PG{Cu-oe^wt165EL%ec zX0N)R_z$c_h?JxA2Ha(|hG0YQ`DkU#!;W@VxnXEOi?ED9L;^SbM}Nh0(v;6o67B-C^eAQhzTDhuET99o0Hcj84%z-L+2mLA)X5Tp*pUIKD8(j;~AJ z1Pt1S4<3VV_>|K1oZP1i{gpJ#8JY(Zk-(1*BUBv0JY2zmpH~c&>lV=ZSz}C8DN@df zg}s& zGZG=8ZHyT7_eI;@c{On-L!Ii#lr3Q{)N7eERI<9Am=1!0tH$EBp)&a%{2#arbB1`U`UmDlt z#NviYNl15CnO54K9e?X9vmfdAq76Qz+sFt47Z<9>C1&IEeP549gtCO> z_om@ugMMF19u<4VKNfk|U+&2WgS}^MGGL|J=zGKO|5yu9ruPZ0g8N6h{&dw|?MB{@ zb?vwqi1FwOXbH3}4q(e$e)oX>M4c2#}J}wDyx8;aI-zJCC;|u=4fUexNFTQl!x%#uTY((kSXhat5gA2 zJA3K0dzai#LCN-hwTppj#+qou$R81-Zq4jdV!@9R8GjbSdbg)5kiErf8ikLR7+h`5 zyfzKd3v>aJ>(?MepNStDI^KtvwI)I8cb4gICRqoiLI)fRL8<&;tV9Wo8OTPd%OW6V>__ zSC9x}B?C{11pO9&Olf>s8n0hO#nNrm;;HT1$f;*Ght|xlZ+JABW-xb(#@j3Lk_d;9 z`ZLe^GtPSf#8Cj#OmB6R()rJq+Dtc+G|rI{U^Q7P~#Suo*@qT!ogo z#RAwWV=iHaN6*<#%C;osGLQO8`iv3wH_2%7++zb)MbK*479#Y#=cGUXbxATJEq5CL zD+k+LISoq2oXKhm7(eBBC$7Z<6^+yTZc|LkQ+$cBnFLttlPZQ>$bg7l9~)<22f$WQ zOn2qC=A}CbHox-D)L%^8$O5a_a2UgDM9JGdY+6BfwXWrpZdp4uv;(qtx)DJ@WCL&~ zX(`$q>8&WZcDcWnRqf(aa>77LPTZ}BLx_S7Fc>@~+rz=WiAJGZxnf1ea*%W2M>7j{ z?)2lZ#;J$PUM3^Vz7&i^^mMvho-9LVGgMPJl3B-6rQ=U(G`(y$#+~A<{3f0M`x^$a zCQQb5W?g+7PgZdptv-_3{VF%xe_AT=ge_u2wh6cG5{o}3oj1zbABCjPXgy-nn+x;W zBB?cTS?raEbv}^&X;BN{XyOOO=49+?L6*nkx)EZdP(4b>ZdQa**)^gRys6#^SP^cE z=P8|D?B7)z*;x%^)7o3%F{XBYEAsgwru-Y>^?ijA zGESmHdyYJhAK-P;g$_yMDu>v;>qLZgqOIZ!>HHy?Og5RJ-`>=hEi7Cltf$~i6DJ||u_eB9%Ui*33h_W%8b)&16iz!DnbMP@kKGq99$pu5C_f*kk%(dkQn!$?7m8ek zUQJq-NPUEeqfv{`IoJBbZXhdoeQ6O>U0~v~blI{I3XAh>5OTF>YIb)yaTjZs_LP0{ z66>C`_v{0_Mp8HDP>BU*_ia<3F(Q>)+s6nm6mzX8Y5Gj0U*oaFl>~h_e5)Y143moT zNLfonK~F+gCazm6PdZ4<;Eznq)h$vhtdr45#k0AitzqNH{6~fTF6+{?q#+CAjy{Li z*@A*<<#N13DvR?7olZtb4S2*mZLF6->s!S9)vU+|R}wswQ}&g{^IyJ=gml3_Cq$zDvtNCnv{wd5*+A zx}c=d&bwixJapJuYJ0^*Tbu~U;bV$i6jioK{7Vitd05-Z<}|dTM~ysP_D%QGo-j#w zZm4{9OHab-m}b8EtGZ4r^T)W9)gxKzu%nHp%iLkv0O-!09>LmEtY+^b`1Mgd9IDh5 zPr(6DV7$MB8J}b7nGRpw>hGXb+-o>>*~!Y?Y9L!D&5Fj%94dY7AxaT( z>UEXynI-1^BRfXC4rHXfU_MWxA8w?D+j1EQ+RB-frAIW4k>;_QDe5yHNJC`t>@*a7JP(8H(r7wfieqoX>qUP2OFcfLzIAaUkiD5$hqIA04Q$ zIO@ygPAz@jOp~-fVeF{$=3r2)61RW-f;%&ll#ulcv%{Z}JvS2TR?5oyKl^)bf3*0f zWcTX!XlWp=VfnmWRa=xILGW`TAo{%HB*4h`=w4A>u3*{aEIE&{<^u}n5_m=^t(a+nfy{sz zCwy0h^Rlou$C@imWGZ{47NPQ|HUz23rA{<^aUNkN~^Pp=I@ zQAnjYik|%754Jv}Jo6cL0$%D}Ey^?s2pb2p6h{ADrwr~K+qsDW57h+*Ld27|0*Ds>VaRhz6i zwH{JOlS%GLRl8>-S8q{=>sYA2l9i$L^oZpSgwY zFzS5ck4HNDv!|O}roIvit=vj)Hk!y3MK5d3aA89UKrsYJmTxRjcjwvx{KRW^diD~o zw=C=+ZVN7=eMDwk+yEc>hSvz(1jPcIp|ZC_=af(}j8Atnr&VE;O4ON?bw}u!3^l;X zL!EDca-*RPsEwO~fEm*&a#d#{rqegndY@yBDGErHl(LkIOkjx)_zD3)BXXq)653gp zZLwQU$U~$1iYyXZ=hjqdXc1rMhi|;^&TSh>vv{JpQ_oziKU-@EZ)=HnEcB}tw*4{I ziOC<0|H#whmH3>6`1t0AQxYDXZWuwW2S25ZJv^jwl^Ew6KJm&FwM29$;G%X@C$zz+LACrM^$r&B~8eoHr`)S4cEM}DJnh)JQBn=-rD zO6475N`u*1B?WxJcBSg6C!yrtv4}Hmdz*EIW7|Pf*1Ph1fV&5>t#=0r zX)`KQuPz*jwNpZu`Lu6pdN2fQ-q>(@&wQbavCOLHwo!QDT-@$(q{me)paT=C5tvtH zE63;VJE152`R%P$X=2~0RQ6qT8g?^A0)X5$;zmr$Ht7-6rYm)5C)+>gm~Euz44)Xt zoBcTuxMT{bPTiYvKxly6EjmkycYe7}j+g2d%<^aw_^bbjNA*>gXNkepP}^&xB~CAR z{6mWA2#K|C24CRn7BelwYSUi%!Joqzd2#v44?`f^#P_D_ziCKMl%fur!RTga(~t(| zf#z(SZ{ser$M6z|(Xc$PQJmQA(6$*Va{hRan}&#xIF3WkRN3X4AtivFL4NE)8Cvg1 zf@djS=)_8C48M}I1XIgeHLc>G%d>eCjp=gsY}F&V@u{><+j=|!?)@ho_grz;`S$_- zVae|Am}S)wp$6axvrS#Jq?G6!07eU{K7M?6cRW|$pB&uuN z>{HLd@GI6VKbq@=jKMUkwd%pgp|h(NkzmTAnMX><1MnzS!ezc=uVHYuo>9g?zU@8b z10{y3%*Gz8tmmq1HQ+&R%pcjN3<%eFk6q^}@VZW}dJm#S-NtSedO1)>q5E4uHY$E8 zrVD0?*uaBU>@WS@2Y|n3_E#-S{Wifrukp`n{IB-+k8S+tk^E;h{-0fqx>5)M4a;wM zKOlyUlxw>{+9*8Af20-~dFhD9{5+^!{}zO&yZ4T0iKN%eW5gm3 zj7531_hf%<94|-;y`@3mPfqr`&pj0g-XU7lcz=^!Ig&xk+7sMu=LQX9X$%v=1u7cU zZ=7Fzf^%$)f7vXTcHW05Kkv3UE{!mcrS<|aM>H%_SIVvt?yz6;lmET7IH)4- zr_4s5^fD|~aN(lzw9-V|_EtwZ7{I0Sm#=`PT5;xda!CO~LmH zjrz0GTED#!tF0c{Go?<@7lW7rR2F~hwNI&oy@(dwWr5z~#`j1@P%M!)5YCmV_FpdS?R%*I#5ck%W-MR2viLTs~7v(o*Ij8j&yqb@MlHt8cwM$B%#A&qbX$-h* z&44Oy?js4BVYB6TZCES7ThZFdezPuIvJ4gpee_}vpNb&hI-ESp^ zL0==d#cL|x^LEk-Fwg8y}Q5qs=BJW=BtX5G&%|~3JeSkx~z<(8Vn5TBn%9q8PaR$l>y3~ zE*Kaz1#1Zj6#l4=hIL!g?%)CPcs@T& zqy&ufd7fqnz)Y^(t65`|B8yhOQ^v;uO%Nyj7^QZH<6wJDX9D-ym@>&FH@63t3H}5) z(wYXN+ORGD_q-3`6#I z;_L496}z~0szBXJinOE*MtcjFLT& zP#leVdFyo(=4n_&Zy81TI4{X9;g{`Jp-uXnnDZ}AA(Ki?oU#UVBwM>WlNi?_kgJ3n zbAKns;={;??R6D0hHZsa7~TyQCvuEaR7;OArbeKng_e#)-3xvzL(A) zH!bHa7wCflqUMk0XLb<=(5b#SF|KYXoj4?ta6S7mm=aKr6ShS4#BB9!QGL;45kbg9LrC8 zHM~v(ST$a6g0_)<)+W?4!VEu2UwZ>^)*7oQM}MBJJ_2FSh1XYB06PK%KBR$ur{@Vy zcLJ8Z;rda;F#DLOxW9byA{bc_?sl_#8iEN%Af-8;M?(RW|qQofE#%Y93f;rsQfF zt92??V5zSp=Zgpd7>tWhg}Q`S$s>Dm!qZ*I6Ko&JT>3I7yE z+X;z04)vV=&dwLvC{}yZ{m%N4_!;Z$cPF+Lf<&-TcS6_O=(nG+3g1K`6@;FIN`!Kr z8y-=%y?yglA(v~4tRqUck7)~S>&@2dE%Gg7Gm;DTng~hi!vv7*dcMq?tRK=0)D>?} zsL5yzY23dFf14Y8|H;w?i!X(Vir~BakigKup!U$jU>1EUu6!gd&3Ed2g(9XrySxu- zEb9DpStWWJwxv8=v8hrS(#(1DY7;7Fs=(rsl9UqfVyUvPb9<$UB`2B!RW~Xpc`Xes z8T{HpQieGl8h#NF za~Vz3yUFuM2*(?VvKCNe8y9+7t9_{W(46W!wv>vO8o}jgePhL5_t9F)X4i&!YPrN+ z?6{d*c12D=MZdUR6i{|Ue=>j;Z92Zefe?KkZ6D1UjX{e)L7Y2 z8RZas`e6-aEwAN7A=5qQCnYPK2z?t32VGyV++%Uh^z1tP`2O%KIP&`D zU~Tdte_|SQc4FQ*$G9p02*Xp&`Gq^#rg~~4T5Q27W7DYjRc}5^(;ltsIz z&e_{o5u25=t(h&`K$b?AnbylE?c$dFTub>@1lN4)y=%Q2!@lC)zZ+!(A;k#{PVUgXMf@JUJLWoPy00c*6y1~GMjyy zBx`L`&n5DD&su^N$7)9sF~UuDagJPHFC}f(vrOvaqmI9VRu3=Z#Q*~2fdHc~0gipi$>ry+vrgj?` zd)>>A#TPy=R5_eFa5U;QDtV&bYTRC*HZJmm=x*NJcw+`fbPkZly^o8_bjbA1d=PrM zbU7csSw6h>jlL$FO13vA2c&slwKsY-K2-av`EftgJ)}Y|{TTfy?o%F+pBf&7wijJ~ z@wW1^K4m;wJV!hNU4@!!n`q&x12+P<0_R0fy6(CzyAZ`bixG)&iLG^4c5?8*zvXnfJr`Pq47>-6_ZALCXU&H*w#@*MIR z4*VPK_YH9*d=B$X^0KdGxAMb3skw+>-t1&v1Z^)wpUF5Y(#tjTb^G+xArrhORv1n3 za(m9L)^yfXndCSQO2L9lQt%)LAXM{3$Gln4GV&-jVd6uo|~9!dANSaIFn`R zWm!IaFbW=H8Uwv??cd$f)}_)-`?YQ<{rPP0bWC=1DM5Z*aZHG5>gOkUC1vhRL3@|9 zStGr7_E}qe7Gpmgbd&8`N#m8j@$4LSQ9R*?#ca{`_xff%Mm}Py;}nu77jh|bR9LjE zf%|8&@DT7GIpDLXlBWb`-1gd1FfE-w+o!~gDy6Ln4)~4-^O3oR>}!qXjgMsB+UBTl z4)-)(;q3FUu<5aRc5i0|2b9uJ(|yNV(>thlnxXcM_fkFHnu=Ore%4}Q>d@KJP}94# z_-ZoLpC$cdaN~M54KAn{S^a5mpk%+c$N7Fn98$B3#WY{ccExn?tev^D3rprGfP zGh5&`qj#j&zkKuS{8zX;{_)SM2enLH|Gb_~vk9Nx>!^iJAmyr)dmixjvf{@yIc`6w ze)$6A&wsYal?_i<^i|yJXF13(dYls6u+FWZHze7;v`GBQYiB4ta;pcpUn})yjMz*P zODXPG{xH<`Km<7yyBDwhifAtS#JX6v_%P5h>&J5&apEZ4C9DFNyxf3DL`(F$%dCN0 zP9dX^oACVbhTfcrG?I2=OfqM|UwK1yC0oU(Qx*fo1NfxJLPkz)`=MaTg`wz#xHMWW zpb*#T?Dfo4k^V$z244X80jR_^-F09BkmcC2;k|h-b<+=#cSt(YGtkF)I)WVMXf@8a zjGQKsRC*a$+mXz8fJ)Ei5*FYpGup>n{SQC(#WbW{@HhhHn&m$X-^Vt_pzYsX_JCyAF9j~&LaXmoMh zhcIpHurSQXFs}u9)CS7NBm7>eW>`O9-6`w1hl3a$U}k#)AO|dWp022Y?JF?V!4NPm zOs*A7pEryf_@JBL>F2S4Cj%;WCOWsYzv74@{7Yf+c)rM;P~9szO#-&MCxXI*5QI4w zxj1Nep=6;W`&m&Dh5_0}fD41OFe_;b36kaQ@l;>#l!% zzCjOvo&Cp=rfQxZ+9tJ@RMvnVp-=qvfvYbedk|*iYif4!a>sAKIAj~>)OAC)qopeIH^oVz{9T9?&vKZHr&4+?dpsdoh#Lz z>39M>$7m+;qyAIuO?Lk=E1^0y_W#DL-b2;ISON0O@sDQ^A_9>PLV8tQ{(kGX1f(y4 z!n9=Q^ih96{KxDm|AgY7CrRjkb?=Hs;V21+#xfK`Fn zzb5$au~C4d&hAVGTLAxf1_=ZDI>d;)oxk5kNsHa|4goAwhOaVO2$C6;1^svMM}lpH zvhoZ+#_kU)cF{U4hDx!5zkG579~4_V+W8I|^oSGIf-yNS@gS|!JKwq)&OwsaR=V>>l6cDTS&<>Tv@ zf+AHPAO3v8) zCsWwmA*20@#}Bt@7gAH>ZKcx5J=?k>1Tqzl>_4|AVECPT{{eq0OIdNdQ=?v(gCm66 znsJXjf{)hh@I@bALy7ZLe;XECxuH)luL)g-eB5<~d@=wr_v`Op$FBRa`1j7=Y zso>b*YFcltzFM}-_OUBpe$@RJVV_j)Q(n2&Tk?W0gGdbeTPUUa1#}sT3Wa5iKR&Sw z0kZe+h|)14A!v~*-ee-!%4abz=^c-d+sU9y>U~ni?1=<2RCWlVx;Vie(PJ*wL&J2f^G%*Dxccfe z8~N`EN|X5d=};W4ZerkfNVJX_?RyXV@6g9+ca=RJ)(+MA9E(Bh?mZwD{oHbb_|p3` z)AYuf1{yVPVx<5-P5kMk+&vzR{NiG(@SteYOK<5+4B;#mJ*ldgWCDI2a+h&uI0%Hv z_e{3}W{xp^rQT(|FyA;}i?kw2o=V~5&_U0oOXAw1_P=hm#3?^O*+xNJ`MvCqhk9h8 zdcT=Zw%E}_#e`lS%Ewa=o#Z0Y-;=Z;VbY)zQS>@PZmCgao^P$yA-D0|6%ZWQEM9Imh=tQi=WcWM#W99S6T>2t$zH{Xo2? z=HBWEEc*k;yGh{v=FUzf+OKDzBBh4C(>U1FZA6hPF~ke$fNbl=O>X2%{ui(YzZ*kP zVZbec-$kz$f88-#L8ffJD~L;&i~Z~LM3I^gLy;+!pN|HDRDN}F=8Ke+w7O)b0d5*A z>zao&x(_&~P@_YGN$RcAyBCYd!?!vLojxZX#NeH~YVy@;q`yoS{9D-JIs&8>x6(J0 zw4r##p#OBO{=LnGfvRtk$3K_szqk-2p9N(GYlb-?6b2Cl$~T?&zkh3V_`XUSf9&=F-dxuU-Xfun<`sL_gpVr%3bwx!UzF&w_C@FQ+Q4!i1$YnnIm8A8# zzsCY`FIHaLf4Hp3DZfxMZ+`T~AC<;J0N=@Li$mPxEW{oO7#pXP!$_n|^K$9zLBK`dtIZBwAMelCxD2}l3VV`tpvMn($ zj1Z9aia;h8ZJ0gYfCi{g@-xG~`A<3yzz%pSaF&^SP$@%$f2vyo(dy#6 zR9(P}FaTEh653X)Jx19PV#n^JYK5sLiEPPEz z17$)d)~|tVEUS4Ci_Jnq2pcm4BPi&Zk^k zdGOv{=_0dvXTv-c86O*XHsbzr}6P#Qo-OzsR>5v)B?A%RB)i@9SQ5Jr?+iI4VxtfzYLK=qeK(*9gC(5 zMy7+{AFS>pDMA`nK$QVAMdJmzQcPJ#uQ;Hs6W;M zL<34BMQQKzn6ej4rwFp98cy`VW{ZV}{GLS|z5${#ya}Q>?_-fyIxM9B@Y4P^cN+?n z$#6bi8IHd4IJSsbW30x{>iC#fSWDW)gW`a<4%Jd%JieEQGh69+)zgRCo9f$#FjRs^ zTZmRTn!lX)xPaR^?3-@IJCul1Ex@}PR^=Pk_n z6aiUVD9td-?Z5PZ73#afnsQue=(qG2?#5tqq2u$`uTvsx;rGs1$Rk~BpL&?VZ)cY9 zxX9)k$`a{}yc}zk|4~TOdY7}Vl`AQG^sJ&>pN57X5l8gEk|E&9GTVx-``m34qK!MO zd>oJ=J2izJd+U*g2yr8~7YdQOMzU@@+4$;}T5X4lH<6Y`Zmks+dAT20Tco7EI#l~( zko-WIw)&g!p&(7V#l_xZ_;rJ8G;?kdM+o&0dt=Uj_~vd@$WR!EnF9~@Y1did;T90B*U&UkipH{7q@1Nj z-A^qQ$)F7OF=xQ^yBIEeDR2kH8wCd-$R#o;CeV&+eC>QVB_Za5*cU}#?hMH%ydTYG z^!kD5tK$#ovsGHLpmIY1*Q4)?`aaUzN<;nC#Sq2l`e>p)A`#OM%=Zw~qU#_gFB_7e z9cJq=Q8r}X%L_AiM_wN=+cjv7%SzYM^9cvmZ<0;^qY)P2{V8wH2u0;J+oxJY?8%j{ zJJA;EqL{3pAz1<=o6WB*+6vJemGcGt2XZ9bl`~OEk9BVW1oXz0a2cPr(@SRF8{x)_ zN}YyU%|Ku+exSY$8~J)h0~F2wzgGFjVn8e+Av+`s?nd0!1^vv{F6kIuZ-1?lBOX@nPH6qyo1$K^D{9G5W1fX5;I0YW zj_AB_$zEz!V1@HZ(s9`tHbIv6D@mvSj3IgFKrj^n_!Oj4EAfQo&{9)#$zMe@@cZ|h z=#ch{WQ<P_;-vz*e&Ma|KWLm(YE0t&!Tc zw3qHmY5J+izJ?>8-s%&MVeuY z?CdPV3#9{md?owIyL%Br)=Y7veu~4$dZr?6%Bq-6gVpY$My zMQ3AU!#HNP+@RP<7o_)l;~52Q)BI8`Xatg?0Yu~)CEx$ytTxd&ZO8D;*ejl?tW5k` zI9P0}0nO5gdnW}rf{_>v3)iZ=P z8m(uHPR#GlO@#|@SXz({+P8n;n7xwMIkGndnSJ)m+^;`x6~9MeIEDu0t*c>;_st5V z8^caDGkLTcCT~p-{2LseqT2AhZIxfve`8{O9ogC3+Hu)$Q{(KqX#_TBbJYzjT(E=_ zK+@l5PmJ6aclf1XE0vsmAmSdIZ^#4>dM ztA49e)h9Xp^-Yl%^KQYAO}-n!i5c(*{P9{Za-}OaA$RVtKVRGfZL}Dq>xT`+tdTE% zI33`U@i_$x%}TqZYeWhpDm-j|Ka||!laqr6CdglzibbRX9$kMlI0#76%MPV`A|zI6 zM1~7{^Q@`VGEpSSJo}55IT^c(1!TmO*tDF($J}7Q)-r|Y3jP4tk(C+esheY z=v^1xbSI<|ey*!;cG+dn_;$54n(Oa5fU-NB1vD*p*;5F1AC1CeR?@zBc5ZufmwRY0 z`c&EWi-T;Js>2ssD$V|OqZ@u$9Gk$2bE5#=K<@IK>8yiO$zEI_gUvZQRhrKkM|9Gj zspSjD^eu6H(8&&OJKUXRC7%U|r;S?}uy{b#n)3+u2hRd+ z;in&Z)y{Z|6rIr}02#vWvBZh65^3n<%f@&!g7`Ns+r*lQRYbCn;xIQ$hDp~o^~Nu4 zP-lC@?L^Og?#W~z*4_wMd+o=_LH*qWw0i0$@@rwh;5QfSZJAkr6i(#};O5cXaAeFY z`L^HIo6>%Km9=~?@7{umnQP80hse~bI-|8ge?sLmzfe_NgtxCyhHWYhQ>%~=eDc0G zHD=n^ec`&A<0TXUdiHqkry4PW{?x7JoC{{fLqcQqmyHw1y0w%O=Qg_f*zsHvQ0-QG zXRUq(ovj{^M&s6dPU3(Ggd2RlThrQL+*Mw*GriVL6}+YF`Hi-cCnQoEChi5b;m{JeIo0 z!T4qLw0VI2hy%jfG5C=(!gJ?4Q+cld4Me-mPx^Q4RSi;=Mv2-H;HAcV;Md7Ww?7ti z=W*9BL;LCuKU6jQ_H=Fgk_4lJ^9PfznxA}Eqn)q5JAnc6!37zzU!_TyY7x!f!)r1? z0-w$q`?Uh=tl+IF$tK!+a!Rr;rGON3^UN1ujpW~R@V(O@bx&>rO$U|1f2k-^#&jWtf#(4zsqoOQd zF=VFlL_v75VK7m`VOf7xT3HBp90_tCXni#we#!dDXJM*3Xa@d=@a?~yk*0#&MO{MSgmSyr$(+}RF}ZPzIF%XLQmuGcf$@f~hwLbv)o zquE8t#Q8=6&o`1p3{^ zELW~@_h~7MaPrC%fANQgrmyf<`TC6c;e^|s&wDwDpPmJL61mV{Gk$q*>}5)b*OB|B zMfYITSxF&-JN~C{#`WD1@3T^`yKuH|YHQIH7iDsWh-3Vm(d%B+C!jz^>6f@u335QB z&3yTGAIgc%z_>kl+4iM&Ig!;KUf_bpu}Iuo=lu2#GDEJ4JGNpd(AnVN=MyGXj)W?l zBF`!OC_1(+T@=QSTZwrKTo~skul=l?=Y3_V@4S}C8WYyShCnRf@k8I7IiL#t`Bxpv z$$$dyc%l?)r+bTXTE9V~KUe$RwCg;>D&XOUT(a>hqL@zDay0OGEBO{!fKIZIQa0Bn z+;HQuDJvRh>(v&z1x(VtB>jRLtXPx`p?nlps`)QAK&ype*{IYDL; zjRX-wBdlly(TiXYKfyM>gq_?S(AcDpUfWQ?F&roT~0m)DIn=Rs{4MVV#D zeiqyVf@*0-IbDx zR)EKp_cm$w)mgoPr6Gk~FDD37ghg{7sc-Ic_1W!gNy6S|u9xoolv2Z7<_7Bp>Jg8C zi9{x%`)X>^1zOwzeOSN4X^Udp{RmBTk813po-owew&(6E!{Y}El{1XkpL>hJ+ZO~R zmj1oDfI1=0Y}XDhU=-GQn|_%V8N&p%yPqAc0&$*jWby^&`arD#mlCQ3O6ZmEs#fJ= zidNNqjL`Ov!Uu0cJ&U&d2L5#so=eV?=>mC<1%qVp@dF*6Nv|53aooNfpedp%p-Du_ z(r0sK1@l+@6>ZkID}9SJV!#6;zxst)e+36M*-@P3TGjm{XNYcRR?|3~i6fB_+xDtlpl{|Rp*L{kcX z!qXI6NSxB>z!WVYz;>XON07+>a!4?-sEh&}u$l3=Q^Nb~k>!rCWUIqAlRtFwBPl~U zd6NnCv_tRlRTSt~M;bLbq{6{=Il5H4N;ZCV$l*2NI;;Lo{hPkDWA>_7rvYcnvb2kI zWM}=5hXvL$E#7vK?KR?uFBa_CMB_AB>Q9*mnDvMmFF7HIho1ffoyVturmWE^2dLs*o&Z2rT;s#(8WswY38FID6jqh=>HTqs8q_VPWKhsRiUIKlW4B5K|}K z7}Qa70wtwAfA%x^6}FX3My)|qzr!4)CFtTBo9|vy*vmp_OTc1~d~*hzSg^IU9QCUD z!1ZN0KDF!@^lb4nzgp`zVF@DVL%}J_wMs?AL_Eo@ZQ5wnE4K57BnkTs3SRt>&uFnj z8yoDaRM2__{RP0mwJE4Lq5tCGNon7I9|>vy9`jZX>44L#R1(~V0r^_X6b@`+jE$`p zeU>p0B;$8VAKo2PbRWho7>xd~?6HH!4D|nUL;ltL!k0h8eEWJ8`%t6QglRWYCeJ-> z6YZW5_28?`JiW5A&`K%;eoO%&`6i3r*8^SNxPZshxNV#v>Y%A@{OQ~(e{`a)zhaW4 zLB;JfybG{cgOyxLx}sc(zq5i0P0rA@EDjoZD~~@`rm&%c{_AFW?uTNSa?* zvVkmq_aC zJX(#TrE+=}4wScg9{2auo4I{|wL4R&>=Fsg_LuOv2&ifm#Ngk0Fb7&pO_{Y97Zwtt z8EJ1G&S-Wi7H@~#3bgAa_q6s#VCs@zo*YtLdfUkuFORKvp5z1o2Z}A^BY56Ca5HVg zJ@qAo{Lt2U@KClV;Hk?|Ml*@)YfT)u1R;RU2V&|ZWm@c4Uga-}IIKbwT5ga#Yu1G- zXi={HU6fK8yoI~v75)dSjctcGRzi_$aGrxysZ!X53sw3)d}Mwzz}5bEkTuk>mA8_N zQR`90k=RW42LO~jG&+0_-=b@e#16Rimhld~d6&(z5?#+N*WM#?tJ2VliWkje*|Tkdek?&*P+HN!ey?uaKs2>- zZP|kF_xfrhXrJe@-&N|&2!7P?Q_73&6$KdNDv>dtb}OI_=M%7!KBHuZno5GUG}&$G zk)EQuy%k$aWoTIhU}`wt-aIt83NI+Lgt=M|wH3RzwMp$)yRu@G@X8P2(S1Cte2hwB z+9DX9(w&A3e-MBb`Z#oW`7JMDUhwDXa7<73iO^zmz5LsZ3-hAV{RO75af2Z9`IAiy zrHRfxf5UpJcVkMTXJ^e@mC@_vm{U&io1vpWVX z2yl3lX+5{ll8ud$_DRI=n50(MXD`M4f>@!-&nALVm~Z@q4H6lR3p&n9FbsG~v?~mh zI;%WR=^`!wS;FvZ{#oK$UKAN3FL_KKm-ROMe@dH#CK+%h!$BCU%JZWkwTvW^LC}DU zqqZ6Wasi@zD-w|KMbfb|>U@NiF%b#a|6Z5oA1O%jjqg-WKtKBud;zZDE~hzGM!MKe zTAt3@SG(pDlbugDsK5wJ74lQWriV&^Mmnqahg3$L7gu;dwzS9$EI#SDNfmhMRWvbb z2v4+1tyzDVe<#KhAzCeTgN3Ybi$9j}qE?P{C(G%V#8fvX)82U2uBPIKLWNGv z8#&R=#D}_GYXcK}w$=1eKXW5M_N~SXXt`*8j~?@(qup>ED$!exlMP!?ld7G@T6ATl zM~r#^6NGAWL;Y6MEELq8l9!&1#u%WhRJO>wq)EuXW9{(?zk=I-3wv_&2l`u=UTk(a zA6DtMD}Gyq;h7LJ3bkW{CsOE^+Jk%%uYIXu8Oxf_@d<7kUkefU))UTdn0doB3tB}6 z2hy}S+ww-MBYF$*rYTTq$}qMN#oET+Hug33P>@| zKS{hYyHZa$>OVjO6d5--i=6PLixw?%`r*de`NwwR^+`&pbT1MPD;|PxH*xAbdW|Aw z_@%ijLnKbLG+I=QhH-HEf1h9vqxyaN^yAIA00I~^c+pz_!E82|GJYggaS;LBeuSI7 z{)o6fsdap~qX^>4d@*>{cc$AKsQy?Vp>8Db=?M4{Tl0E6l#D_qsiLNyZS~|l=2Bx zWP`<${Ro;Y&QEjfn0o^hyL&v?5LTL--0d3v@dI}k&wr(X*^L!I7iuYS(hj>qr4?3< zB0dDrTd!ha@DT`2*pt8wWl$HnlR-|5$ImQCs+8#aH(!q&9dJ#qrMwyiKs zF=HzaO{TM6g+j`}^^q;)CN%cBSD=uv=oo?4{ z2o>#CIs;g*DHh>twXBwM@vhJI&(ASx9^yp&rA3~>Wco}5x#O4~wI3-0Beap`MmI`N z#OY?wTSyLm>ZcY9Vp05ljn&n?VzE1{Sxg8R_uD&sDJ?e%g{^8sdhkjfuN9od40c%Y zFlE0?=J0Nq<2H3VpP2yCPeI5UfwJht29DmRFQD4{4|8lTNI&MvF-1O?o%EFY@=9+y ze~1^k=}EA!{p?wCjrAaAa8Pw!H{5rFQ+Wm3I=9;1+^}njS>^gUpis`1cx&S=WR!?P zfGnV{#Ms|3XZgy|v!Sli7RX{)E{FzL;eH%i37Ee67?`9&}g6^ z3CG0ubd_&;_!ib+W-SqO#nILWks^+Ssp?wh0dk(?=7Bp{-%aszJ{CS1d7wEsV$-Uj zixj%If20=zI3B6um%MtoJ*y2T=B|os5~(t{uDvWq&pBB0Tq|qi&nvC`$mzVm0u~x! z$&RyNRlVBd<&Sp4>^fir`nHC)e@QB6s8~@)Cx3$Pb^!04b|m%%TN)^jO}%C78JZAz z$-WdL`yH2NBZRo&e>-z|W)01%xmjNQoK3EJ@j<{NTr%zCPremsz2p1FU3BHiHMa`p zMx&3wgl%11(Ny>E?s-Dd8b;Fhnx!D*N*1HQY#*O%-ixJ)Z7;Hfd1yyoFpgm3OI*IZ z!vEOG4xc&nUXi@)^rS+sUNxM1F&5ypf~c3O`otgS?2B5x@Ftp4t-;Pa5vV>PSXf9$ za@vZtSJUU>F~M?P9K)Zo8Svt#Da2|*mdWlU66ne1`z@@7I{ZZo28Jg$6^-7kWq+p$ed)a8dV(>ST=yD|*H%;HCk^ixE z8?i#?8QrTts$>)qkL|`5wyhIkX+$`^v5mRT{Cww{2T^g1OX`|X!s;oi)rJ+k)!OhB z=OpFx32i^{E&I4sYFtG{LS|gTUG%Hq37Q&8RUOI*B+q+W*VdXC3E*ar7s-7b{w*J% zn#iC%gT4UF7rZce+jd=U*tk4gi*77Y=9DJMJ1Ws%Wq2zOIf&lSA24&nW>=$uJQOpT;28V4>OfG)L{ zY~X=eZ&x9a09)4Q^S;})TOf5R%J~gjvJg5|7XW@;!pQ*DHA1%cm{_%W)Rk1SL9i(hcl&B{<7>mY`j_-K7BYi2;MZX<$V%nrY#2z|7L+siWv`i!d{^rBrjB$r zV!!=NE$l$tTH*c(sxWM2!DH4^uorMY!d1)?)*U;hV9s6`xihM^-N)d1Ll)}o3`!(l zXA&S__5gFPn?9g&VctH?nlmmJu?DfD z^YB;qIgB{A*LlLV|OTaPPOPgRPp>wMD(yG0pmp*m?3 z*Var=+U&EQTX19w@G8LQ*}Q?;HzTQ)zN7UeO=$91_`UKnJu?I&k#1q_f_(u|LdRw= zX|0V_e6RfxRKn$)N_*)+8ymr`8ppQ5qU2!#O^fg@-RlXDm5YZlYErPQTc0zXR@|*G zJXsLDn7K~x6AdRDt_)WCrFu7UZVko;71^(&9876jEl?k{O~vs!^ZTPULjfj=7h%uP{vjlPQ)a%YWFj-53 z(wsu8Gt@7Qxb;SAcV;?&jVwaymkEGLvELsL1ZILDBf-vcTUX1NfoSZ>KuI^~yfC(+Ew3DH;i`pC`%EnNa* zP1eq1`T@@qNQP*lz6@sD3*5|@E61V|O+bSvfbmG?&au(?yDHn+v$XBH)9T|m`c6;Q zvQU`2*Fn4qJntv<2%O#52NOD_bjD#IuIry4kv;M9WYZ&BpQwa_J4BmWQ;tzNw!*jI zf_>Z33zOxdXnx6~o`oTKkn?94dBWuy+Q%!FijKBS6x#+#w~-_|>vI&w3tg~Nuj273bhZK-ODJ;jT`cnJewMEduwa5Rb6gp8DK zzURVnpMtQ(Da$D#S={ci!-#Oz`^HXRzRjXMAFfzptxbU(AeOf*KG2$jpIs`Bm~O?BQRT#_WHsVidh)FpA@W|e%UKS1^rQxI`!D5BSvk5 zcZ*~9mm{&8=jcGHw1=Ajow042QsYkP=HAla%lS_fCwVIpLM2b|n59iv3rZQ>s$<&& zZNR1eiRT7BMpqurR1$6pL10MxsI573c`qIM%sqs>pA{a-BZS{iuzGB*dGD)*#{|q? zJ2H7Mzo+%F+t`P*-pXav%z<-*@W6&I+$9o}3ep_{v0=S)vs~SCoZr2fgoIkNpKeF& zMCj7^j*y~3KG%n(Xc+Fz&g-LD4`DvS-7V{W6x7kbLQGB<*26Uro}Fp4Z*L8z?qBnx z4_puFLqo)Glk1BjrXdn;Ki?+LKc6G<9EQ}$fh&*6$L5bhDEy}1*g(T1(hPn-IeYG~ zZLfyk(%9M*8)IH<97Mf&oSR|Bh+dJbqx&>)ed}kjZumo_j#PONM^#1CtZfEBd)$J->~<&q-Qf_&)Pwi9DPWYcVetYq)n1rn@}L zQ1qp5(vsOD>+)K)XiA{ZT^q3T5)o5$?aI@T1_$ZOe|HL^C`l->#^a8(O0dRt4wgdP zt8b(t8SOSf+Kiw`nj&6tx@Kz{gH{hgyhvj)=Ow^+VI`)?JPgx#k>rv>XWM^x@fX*MlFdkm1=Iz+lVyy~( zn>A==E(5LN?9)E4W~qi|#*tIB+vSdIQC*xcqzRGAGzO@X&f@__7x%tC|_7tRD%GzMk#7mTf6q&dy7$3 ztrdGqY#|5|2_bx+ulG5xbI$K`{r<_7D>zK-#_w@yc_%0RTiyf-!uZGBO`iObR*t0^c;5$7p(T3a15&-?)nU` zFl3^xMta_HABim`4C>-RSZHx!p}w;$^@0rH;wqmLyxE*TOznt0-6+2y9E076u57@Z z+|gPAk#Fny^Cu-Zd70g097>6KYx`j=ZQaYP!(NP0Hd{;Et@K4R!r)u;^C`7*#Mjh>}G(KqQ5{WN8rivmz5hCAwepGZw zlsb??oF~UBm>f(;d;(GKKQk@To!jR&Nm)BN%*Rqv{~WqPw@Q!TNnL%-PVsXm!CvMO zXu8r`TeuVZ1O97u`tN{Tm0)giJQ-}_KsoL}ITGA~bIY9oi_si=0L#lv;E{%4LJysf z)0%eT;_uVlZ@kG;Zw>B(91eV270RJ^0+(9r&#KiKrUl!BDa z4iKbtw#tf~RsCN#)*VdG`hI1I6XIrc}Mve4vJP@12TYk-CO>-@XK`e30AGV2+Zql@(YNueauYx2m2 zExn=fAFZ6@8lH-Vn3J)7o_5b+w&eXRk|FRdb(G)M(#mwFsHYNL?_7ED+Q~e$))t&0 zxmViX3lS|RYi!xNM>a6YOajbT%q-WukY3pW%s~OiyZy%>{FFGcCc^W*obUHml59ZZ zjWd$E<-_Mojoe~TITC2sR&<(g4kou#(T*>tf3(uhU%I16{x;_$BExbYWD*QA{cXUK zNtQLmP&Axc(UDP3zD9BFiSg@-g6VEYFF^M148I+ZUPk=}xTaV`X8U2kjMH8U^bKf52SeZBKO}gwY?nBFTM!-OVSbbwCr=tfyHAGQq+n}L@z0!|!j3&nFvwc;pX}#h zq5d%<&g9N$TrGxjphB)Bk-+cJ@2>h~GFcsRu#11s^YfEu5$UcEqXi5B>s8IR(TO~7 zLNbS7cDC@~nfXZo&Tb=0M%LE&r8U?5iT&40@UQagl9aUP=BVo3n^7}SZ&JrNV`W5@rAAxZyo56|04c^yIF`b9XN-QhQzM5v?;39$I#}k& zO4Yx_b$p2SUFUa2nXK0g1*|D04EZ8Y zo~XzFlpF6mP|3a}J-6_csb`Hial1dmb?AM4Reb23nJFLZX@9_B`0z|sGWPcwBPkq1 zz_Y!-KE6kcQ;n4G{XMBCux~b<_LIw;BC4`hfojKvR?wI@v^7g5fgZRDo$pq%8%)k0 zNqfa^Df3#jgM=8V+c|5Jv`#O#ltCQxDhIKheX4En8M&sX^oy8#qsKAq^1b=<6*BfI~p3V^gd?Ys}*@Z_6-Xg$-kW@ENt}9;7e1*g0@mM zLc>AYV_&{inrSN~v1~Q_g=S&F_jg(^S2~Xo7da8e$;JAgJ{a^ed2dJG^M9yQ^*nk7 zL?#;eo@Y*rB!5cniSf%^UdQ9^OO*(*byC7LyAKBFYvh!x-p08r_h(26iZrvf3dO4K z%>7x}Y3d*po)?^}gftbeUPG1o_l({PfQYL9cw;4kRV1c4i!xS-YwKk1AB}T#>;Wq7 z3uv2}chU{Gf``Wk#)e`uiv}*%>8B~SY;k|pvQ`6XgwD6}&bGA57$%MJtoaWwz zRq9K~fL8OHiMBJvXj8)_Wn%y zg-bnuhdH*dSblL@+9e~){|89F*)H0CndCKCWqAdDUoPFcKf|XM6-LR`0m*vRPG zeWN*5=g&6_Z*!luZfP%wj1j&zr;SW?{1?dfrTSHIG5otu zzeI7U1>2VdrANa=3R}g=t?16+)EPD3jfkj=^c@L5!wA-a)h7Lo@>%c)Uc1`Q;#aR7 z8(IZ>2l(!(Q1<|5%qtZlkN&vAC|kru-**h^AK8;NFD`AyKQ4ae#w4<%ywr9)+pA`$ z*e_T%#&@(aSpD|0H@S<2TY~}<1eCL@uAZ{N#sPZ#LD`lvTwyn~QI!YRUOo3KoI@SE6|l3jYLoJP2p zgU9Z^?V8Sj(HEt>H#!)~j(#-i=5vGz_y{jNO5b)hp!j@_aQm3|C(Ql#oqI?UbN@{S0rgP)uq z`ZvBg>uJopuY|D;=7IY!%{x;e9fp3qg$&i*JGddgy~cA}(LQ!4@S@L$5)&!E+4Abm z9nKDsScQ&L_DzP&`~S+g{w2)0$b^jlsrq3sRBt4CxF$6+jQjTt`BNqMvHZ@gkx`kf zdivUko7gh@^lgcp;Czh)MS+3Ze2pXwwOD5s=$92-RE3N12muC#TkVN|YRP+ck8=OJXQ-5!DGPT%#?`id{$I1QNHil)zqod?}-xmvaeUNPNoo|gH z&kVkc_k>7$Iw!Lv`Q2|lGjBFP%~YIbzOFdoe{t88`(O4*{{ekT2nYUy-H(*(BqKLh zUh*Yo-cN(w;+IcHy_S48kxP(}_+HN!IPqJ`VJY|uO`jCv(exN;dW5+$XlI&KpyhF` z9B8Y_(T&;iSU61u{v)TDI-+Ls@fF)J|JaVw<~y8ojtY5+cFBNr{?@i61o1^Y;$G z!e**{GtGXTEmQWMTDx#uXA2M}Yu8=E>Lz$07j(3}rPFUbQ>GhUy_KL;Y$cW`tj^kB zvT9#q1%0rza1`V+_3JO%Vd2t64eR-U#nt^%D6+Hv=BVe=li6Dkb%K9Yd9PmOkunUG zt1%SvC5!yHWky|NBd_6BF+Z}+h}f9DE+ABixM+6GGB%!9HU1+>JW|W z=~BzsBHrJSF?~*sg|OC9UdTpL!j1H}-lOrS%cE5(%0YmCxAB9j0P16YX(M_bH4oYa5PfOv%g;PJe7)3 zpyKtM!j+6e`QZ!>?PBfhXW2oL2)mnp!GYU-E6xk6eKC&bzs8*5%%Ubfz~_C5RI61` zHlYY*2gvT=y`<*SSD67gq+5!%@EW|y(m2hZ=c~_D({K|YS?0tJd8%64RZnkY0liIr z&{YmS)XT)Y>WVK!ii=Wl zgzs0a0B!hE;c;Q057$4XIi>p1-WlEWhlemm5E&MKpep+-MI>lIh)f_y1Ad@#@f@$1 z{LmUr_gnEKO2Tfyk53c|RNu@52-Vm*pxsY$S4@9wu|y6c$A&h^_@4C#JkDvh2JJdE zJpHnyJUHRM^Z11?>`K!`t5X+M)7b9~LirzzKDFCOIN1+h!*$5_QGW{I5XPUB zT(wmMKet&kv?yPmV(-0`&MgUgOX|m2%3SmWsIpS_~ej0sZ z6;azywLbp84KC0$DwAUwP45GuEZfN`wB0qxdx=(Q2ZC2KnJb8llX7%Ru=d;M^! z#1t~^+E82J8q`cGjLtp+O#v`V?><6Mys zUyjm$`N!m5%m*B^mf&B%H|ad*Gi%4;A9>W7=aOUhkE(TZ?=`xj+z0NH5_X>tNd8yv z-C{_&K9XsEXWVEL4HsPmS%=fiEuZ26SmV4>E-`QR+xx_OYEyU@zu1UrM|@WBtmd+S z1Zf#GYpETdSNHzz7C%?E6e3_Mx`-S*O_dee4LVx)5YlLsSo4?TbR$t1AkQr?cAtog zm!|LDU+CgnK!O<@tZ^Q7!(5*EOyaf-XH)Ph&*&^60PFxsr?#npcJ^HYzdyxq_xr4*#jh!(w(;1ZHZL^bm`=0$>TD`S21^fhR&S+P~!l=^OtbK z9YlLR4ITDCI{SCl)+y{$NY)hQQGrfAkaMyXJ))iH0Mk4A{Ox-Wp)U-aFczUiH@f2* z$sYlQ8S|U}IO;!e+}hILHXQfYT29=!`SsTJjfCg7*n4=$eiiFC25gk79IwjX1m6dI z$3>1zL#oQ0eUKe9`2;6cYCIa{emx)uq$??<{oZ_Wt+vZ;C@95aTHfqcWi}QpuT2;R z1NhEoNJcKYxZiP)#G#zd6YPW89Hu_Bu+*)4!MWpV_HrZc&uzOg6`y~oD%%yBTTaYjcW7bl85gt4!slBb`Lxf1m>!=ee=11SqMt0zUc1`Nmw4~U-Qj%>CNeU) zXSb*R)m}j0{^nSD<+gBF)f*jgVbj5$tm_-6A$89CaEk?2iMG!dnDfE1=-`8g%rJ-e z4 z<--G&@&QmOy{tNGCY?UY&>Xj}0t#5vFZSWP5ii=TPIAJ}6x6@*>*j<=+*B1I!_M|g z?5^Y*!!ESASo^ke@4P{Zd5oW7r`|?xJpiH2?KuKEA7w9~v*`Q31lry1^6a5QiB6$;;u?Ko*VQCgwF!Ie4Dzq zi=VQmT$ZZ{A&evLk0p^~7fYWcXl}*J4v3yjJ9v=t=(hU8oAPh&yvN%I>Czc0C!8x2BNz3r|F|j5<=)+BMu$uP*N2}ga3a3-7pkA_z-m(uWrqav_FMo0<~ zC@AqBNl)KBA^+nIw?%*6M{~EKnrJB?$L(wn=$PS7<3L0xRZ;nffU(f~Pze~j}0ZXG(9 z1|*%i+ouRyD`uCceYlVZAcbToCd)?)jJL#>=(%DCIx1{G3SEOypU_sRlFA`_AXXFr z*NYyaJh_NxXw*pX?>|zmkUTl{6eYuEMLRpsf2_1UY5=u|NG+bhsselHlkHQwTD7Sc zZJvwx22C@>54EIHNOf@fu^F>H503#!&&|y__Q+4WKk+aXaFf-Kd2HPm8#v6Sa#vTVD4yQb7i7)7nl(&@upL?zJH@M8u3MK$z zu78wa%O#^J2gHQIK*>Ki-pl_%c&mWRyHxAekkXb2}0U z$AGtD*i9TqUeux}7M8_>Jxk7Scz^s#!0e&$RGI!)s=i<2Y4@2%vgLG&=g#CW^rf>& zD|iH~I_Q%s_0o?RV~b`}9S4T{g^h26v-&w<^=?-Fd0RpsIHrh?6edyH)(0oIrQUHM zXKwHpbEq%RDt)|MAt9&Z!);v4x1dJyDeC^H670WsZf&Xz3x-Er`(b~xSOP!e+v8!@ zj!*&$XSwib%_Mc7+SEc@Yjp8{WIu^k)N^~J1ibpIoPnfr_cPcVn?O(Fc6}7n(53$M zsok@Q4R^lI$+KfM-ujzu%u2@dQ@?=jwK8)Ub7&ras42=-7x?y96CR_KE1I0m+!7>i*iR)0!HN3*fqYW^%23^;wFaY@U}(xE2vCo zXoJRgRg5H5Fk?gPQ$33%DGwhqXQHkBf+tIAW8ySzLb-~@UNHd9mMrTgQUrjwb+<+{ z^2L@fE{J$U8M6n5)y2x?BX5q>+)6GHw~&EB@;rWI%PDWt?Bv@Vuz7&%wA(D*vi5Uh zaSE0HwMoBuNkD1g`~rKHU$Kb-&=;Lg#)Ms;AvIDo!Qt`UTi~d;xI%wVrqU_g7KzSQ zdh??tg{yvTQfV`xfE9Y&;ryr5wOElbT=*3RG_$p=Is?^rr-$TNk?slMd{4jgN*VO~*sChlBTSTY2lb6V2T|uOM(5i( zTKqPD>H9+~{Y)5-26V%~%=f1U_(9g{Cw=AWK~g>W2)Am3Ei*~kGJDAAXWRTRegi(F zqiK)^`B_`ICDE<5@cPp19~+B?0}T1A{Eu9jwDYQ0s|?Wx zHBp3g#v3G1I7Jct*i40K#?EA0GUDt*sWy_*-*+-&T5FCaMO z^!q}X=iQb9N=^|$XP<)p?&g0;lphrEtA$|4`q@#Zh2EHCrMa*@;ep(U_{EOXr5(## zgPl}JYG$FA2Z1p)t_!ct>VAn9K+RVA5&$o4$bG&4jykC0c1J@i=NMjC@|G>MI4Q8p z1ezREtC(~Fe&0esj_&~h!XONIl7)yq*BOQM!*}W_8Q?qkde6HCWN4}zgzPt8C>#_3 zak>YiS|xy8a0;jP(!P&&zg)L;X6vBW$yS_mH3LZjNkD@9kMrn0#S5U|9oDyQ*pxPd z#m*LY?^%N|?#w!vK3wbW?n9P*ik5{GE6!Iw0oeg%aSU9^zUO^q{7nmehq5%l(UvQs z)qu@wCNWJ!=0U6;y|T|x&QzEh=0Lb1L-%rzW;wM;pmT_{AzG#t=@u&}3K_q^K9U2g zPn<3p1C{&a29q+3N;&ZCyPzf~O!+PA8kg-iHJC=L4?UiVNtw1?Nw&V9gekSD(j zQH=P68T&!MdP?~k2B<0@?w7+od@em=$9&{6M8S%11X=mIv_eqSogD*&5tvdWVEZVyl>^F7} z1li2jZ3qjx=-hL9<~7sQAD5X|aqM1?oUep14LTO=ji1`=Ox6n8PsQDVi!;c7zRl2o zyAg-F6k(Tc;ViGbQky6UtGpz_1m&O2#|d!!ilG<1CKkRHsO&G{t25K|k65%~KJgkn zY@xx$#*!}!vKjmy3Ko>)Un6G`o$YnVezdY;{lShwD&U_O30L8;ay#n;5Z4~V8QyCN ziNK3G|Gmaac>&yT{MLg^m9sx<3hHi5xyk{An!uyWlL7r=YA@C*PhNm@;`jRvVaNM| zMKqMJM}%;^hN}5HsvEyuHRTK%>Xl@Et>(A-_h|#9|Fs0Vl*oOv>KKS6$+H{m%&p&5 z;I#RT^*1c2vHjDJV@D6Ks@)-l}@Baz@2#@@-|zTaQ{UG7~Psp z!1)_iW?vq@PnU5?XVkD}UQ%%3&XfALQt$m`CV}L<^!7j0-yHZek>bGI{$;77>6r6` zau3l3Tdj29av#ZH&KKw1F>>D7KE1lFct!?$r)Rn^`lmbFv#<0D$)oT!!tIifi+Foq zJq3;tb{i_T_rLlw|LEI_M0}Cw-S{yf2~_Cf=N-%ZRwZOuQPEbT_U_R}`>0{I7gw*{ z&{N+!K_2Y?ks2Bf-+EJ^H%!10v=q6(zGgS#S&kIxMFIW4lM{#@c=s+Cl z%EB3dv=}zK-|Vt}_2ZiE^&XjAa zAG_EBoNJ(Dm@$dMlhM4(dWVtGb-LdUe>nx0Cz06gi3R|MdWGOsQ%2uRskr^$P4M?_ z~AK~^ECh^M}6be=1{5EDM|aAH9E2$ zvs;s6b)#uu;*z_gonl6%KY=}3x-KsS9}iBtlrRRJM}@#8#H9;qB5M|o;g;1j066MB zVmgyfbc07Na+|1|Y^$U=z$~c;Y87W!1J63KAEf8DA#bC7CQDVBc_ShxWGIo|e_qK~ zuW<#D2RV$Bz;_r9_TMqbp`=4!3SZ+hxRD^+v)0kPl{NQDwGh&PxltIx0r|x;c30~i z8J17gMei&7(!KM(i0M&-bZ#JlFiBvlWSB@-vMa(m5dL|erfmok@j;Qr^#jg(Tv0ax=3Q(yLsYpm?4c+IEpe3=54x!$8=vE2-M+NCA zN}z`RqGwegTd@N|(~o(Oy6tuTS#Zs1HM)Ww8*o-AKlNY!CXL7MnyHc1mFC%2{&Oo! z3~#+DKYsi6&Fcy8ZkCK~CvRZJd5^RiKsQbMSkn6D2|=xabEtYvNqhK7WmvM%S$gDa zR-3(BVh8$^Pt8I}$+wYXy;!VR*mVxJFvg$$S4GJu9x8Ptwck9)?TJMR6KE4KExHPS z;9HHU^xF;H>=*zo%`VdRZR!sF5;)r3Uu#MOnmFt^o01O-WXJ_TfdmLjl8gbXx+F&* zvH0Zb4SBBDz4YcUk{{)jrX3#@?Tuuees<>h0~4nYmD+JK*eY3?RUqy_P#*xs%fCb9 z`%r$^8S=g}6$!53y4WPmRYAbo=`!^fXoU}l&$US)fD!0>u|JEZ@(@|Vj=hKib?+Ks z(Z5PbKEtUalWyxYr?tJkNzId(4cY9Pd^iy%u=A!%h{k`H-}yJGe0a}4X(+*HLkyw+ z?rD!u8Fo^V^shM@F*S);b7^9>1`q-ps*LPFXL?sLS^1;GnBNs?)ZcSSRtY?Sk3-RK zgadZXfPQMF4F||Q9`$l8F$F>;PX=##-w+SSSd8)XE|ZUTWaZNy&;9h7eL+j|V1G_p zW9SOH57Z?w-}Z`JAcyl1$5QtCm=&&e?Ra}eVbJj&I@V9gQ!Y_vKC1XEnO{IEu2-!~j}VhB<^}vCsG61pb_Ur05doHgd8xV$9C|Wrj

    - { + onEnabledValueChange(event.currentTarget.value); + }} + placeholder={t( + 'dashboard-scene.switch-variable-form.enabled-value-placeholder', + 'e.g. On, Enabled, Active' + )} + data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.SwitchVariable.enabledValueInput} + /> + + + + onDisabledValueChange(event.currentTarget.value)} + placeholder={t( + 'dashboard-scene.switch-variable-form.disabled-value-placeholder', + 'e.g. Off, Disabled, Inactive' + )} + data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.SwitchVariable.disabledValueInput} + /> + + + )} + + + ); +} + +function getCurrentValuePairType(enabledValue: string, disabledValue: string) { + if (enabledValue === 'true' && disabledValue === 'false') { + return 'boolean'; + } + if (enabledValue === '1' && disabledValue === '0') { + return 'number'; + } + if (enabledValue === 'yes' && disabledValue === 'no') { + return 'string'; + } + return 'custom'; +} diff --git a/public/app/features/dashboard-scene/settings/variables/editors/SwitchVariableEditor.test.tsx b/public/app/features/dashboard-scene/settings/variables/editors/SwitchVariableEditor.test.tsx new file mode 100644 index 00000000000..055fdac1167 --- /dev/null +++ b/public/app/features/dashboard-scene/settings/variables/editors/SwitchVariableEditor.test.tsx @@ -0,0 +1,155 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { selectors } from '@grafana/e2e-selectors'; +import { SwitchVariable } from '@grafana/scenes'; + +import { SwitchVariableEditor } from './SwitchVariableEditor'; + +describe('SwitchVariableEditor', () => { + it('should render the form with value pair type selector', () => { + const variable = new SwitchVariable({ + name: 'test', + value: 'false', + enabledValue: 'true', + disabledValue: 'false', + }); + render(); + + expect(screen.getByText('Switch options')).toBeInTheDocument(); + expect(screen.getByText('Value pair type')).toBeInTheDocument(); + expect( + screen.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.SwitchVariable.valuePairTypeSelect) + ).toBeInTheDocument(); + }); + + it('should show boolean value pair type for true/false values', () => { + const variable = new SwitchVariable({ + name: 'test', + value: 'true', + enabledValue: 'true', + disabledValue: 'false', + }); + render(); + + const combobox = screen.getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.SwitchVariable.valuePairTypeSelect + ); + expect(combobox).toHaveDisplayValue('True / False'); + }); + + it('should show number value pair type for 1/0 values', () => { + const variable = new SwitchVariable({ + name: 'test', + value: '1', + enabledValue: '1', + disabledValue: '0', + }); + render(); + + const combobox = screen.getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.SwitchVariable.valuePairTypeSelect + ); + expect(combobox).toHaveDisplayValue('1 / 0'); + }); + + it('should show string value pair type for yes/no values', () => { + const variable = new SwitchVariable({ + name: 'test', + value: 'yes', + enabledValue: 'yes', + disabledValue: 'no', + }); + render(); + + const combobox = screen.getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.SwitchVariable.valuePairTypeSelect + ); + expect(combobox).toHaveDisplayValue('Yes / No'); + }); + + it('should show custom value pair type and inputs for custom values', () => { + const variable = new SwitchVariable({ + name: 'test', + value: 'on', + enabledValue: 'on', + disabledValue: 'off', + }); + render(); + + const combobox = screen.getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.SwitchVariable.valuePairTypeSelect + ); + expect(combobox).toHaveDisplayValue('Custom'); + + expect( + screen.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.SwitchVariable.enabledValueInput) + ).toBeInTheDocument(); + expect( + screen.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.SwitchVariable.disabledValueInput) + ).toBeInTheDocument(); + }); + + it('should update enabled value and current value when currently enabled', async () => { + const variable = new SwitchVariable({ + name: 'test', + value: 'on', + enabledValue: 'on', + disabledValue: 'off', + }); + const user = userEvent.setup(); + + render(); + + const enabledInput = screen.getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.SwitchVariable.enabledValueInput + ); + await user.clear(enabledInput); + await user.type(enabledInput, 'active'); + + expect(variable.state.enabledValue).toBe('active'); + expect(variable.state.value).toBe('active'); + }); + + it('should update disabled value and current value when currently disabled', async () => { + const variable = new SwitchVariable({ + name: 'test', + value: 'off', + enabledValue: 'on', + disabledValue: 'off', + }); + const user = userEvent.setup(); + + render(); + + const disabledInput = screen.getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.SwitchVariable.disabledValueInput + ); + await user.clear(disabledInput); + await user.type(disabledInput, 'inactive'); + + expect(variable.state.disabledValue).toBe('inactive'); + expect(variable.state.value).toBe('inactive'); + }); + + it('should not update current value when changing non-current state value', async () => { + const variable = new SwitchVariable({ + name: 'test', + value: 'on', + enabledValue: 'on', + disabledValue: 'off', + }); + const user = userEvent.setup(); + + render(); + + const disabledInput = screen.getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.SwitchVariable.disabledValueInput + ); + await user.clear(disabledInput); + await user.type(disabledInput, 'inactive'); + + expect(variable.state.disabledValue).toBe('inactive'); + expect(variable.state.value).toBe('on'); // Should remain unchanged + }); +}); diff --git a/public/app/features/dashboard-scene/settings/variables/editors/SwitchVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/SwitchVariableEditor.tsx new file mode 100644 index 00000000000..3656231c50e --- /dev/null +++ b/public/app/features/dashboard-scene/settings/variables/editors/SwitchVariableEditor.tsx @@ -0,0 +1,55 @@ +import { SceneVariable, SwitchVariable } from '@grafana/scenes'; +import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; + +import { SwitchVariableForm } from '../components/SwitchVariableForm'; + +interface SwitchVariableEditorProps { + variable: SwitchVariable; +} + +export function SwitchVariableEditor({ variable }: SwitchVariableEditorProps) { + const { value, enabledValue, disabledValue } = variable.useState(); + + const onEnabledValueChange = (newEnabledValue: string) => { + const isCurrentlyEnabled = value === enabledValue; + + if (isCurrentlyEnabled) { + variable.setState({ enabledValue: newEnabledValue, value: newEnabledValue }); + } else { + variable.setState({ enabledValue: newEnabledValue }); + } + }; + + const onDisabledValueChange = (newDisabledValue: string) => { + const isCurrentlyDisabled = value === disabledValue; + + if (isCurrentlyDisabled) { + variable.setState({ disabledValue: newDisabledValue, value: newDisabledValue }); + } else { + variable.setState({ disabledValue: newDisabledValue }); + } + }; + + return ( + + ); +} + +export function getSwitchVariableOptions(variable: SceneVariable): OptionsPaneItemDescriptor[] { + if (!(variable instanceof SwitchVariable)) { + console.warn('getSwitchVariableOptions: variable is not a SwitchVariable'); + return []; + } + + return [ + new OptionsPaneItemDescriptor({ + id: `variable-${variable.state.name}-value`, + render: () => , + }), + ]; +} 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 642b6df7d92..2d9b07c1d38 100644 --- a/public/app/features/dashboard-scene/settings/variables/utils.test.ts +++ b/public/app/features/dashboard-scene/settings/variables/utils.test.ts @@ -147,7 +147,7 @@ describe('getVariableTypeSelectOptions', () => { it('should return an array of selectable values for editable variable types', () => { const editableVariables = getEditableVariables(); const options = getVariableTypeSelectOptions(); - expect(options).toHaveLength(8); + expect(options).toHaveLength(9); options.forEach((option, index) => { const editableType = EDITABLE_VARIABLES_SELECT_ORDER[index]; @@ -174,7 +174,7 @@ describe('getVariableTypeSelectOptions', () => { it('should return an array of selectable values for editable variable types', () => { const editableVariables = getEditableVariables(); const options = getVariableTypeSelectOptions(); - expect(options).toHaveLength(7); + expect(options).toHaveLength(8); options.forEach((option, index) => { const editableType = EDITABLE_VARIABLES_SELECT_ORDER[index]; diff --git a/public/app/features/dashboard-scene/settings/variables/utils.ts b/public/app/features/dashboard-scene/settings/variables/utils.ts index 684ba7b1ef0..adebfc52889 100644 --- a/public/app/features/dashboard-scene/settings/variables/utils.ts +++ b/public/app/features/dashboard-scene/settings/variables/utils.ts @@ -18,6 +18,7 @@ import { AdHocFiltersVariable, SceneVariableState, SceneVariableSet, + SwitchVariable, } from '@grafana/scenes'; import { VariableHide, VariableType } from '@grafana/schema'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; @@ -31,6 +32,7 @@ import { DataSourceVariableEditor, getDataSourceVariableOptions } from './editor import { getGroupByVariableOptions, GroupByVariableEditor } from './editors/GroupByVariableEditor'; import { getIntervalVariableOptions, IntervalVariableEditor } from './editors/IntervalVariableEditor'; import { getQueryVariableOptions, QueryVariableEditor } from './editors/QueryVariableEditor'; +import { getSwitchVariableOptions, SwitchVariableEditor } from './editors/SwitchVariableEditor'; import { TextBoxVariableEditor, getTextBoxVariableOptions } from './editors/TextBoxVariableEditor'; interface EditableVariableConfig { @@ -117,6 +119,15 @@ export const getEditableVariables: () => Record { }); }); + describe('when migrating a "switch" variable', () => { + const baseVariable: SwitchVariableModel = { + id: 'switch1', + global: false, + index: 0, + state: LoadingState.Done, + error: null, + name: 'switchVar', + label: 'Switch Label', + description: 'Switch Description', + type: 'switch', + rootStateKey: 'N4XLmH5Vz', + current: { + selected: true, + text: ['true'], + value: ['true'], + }, + hide: 0, + skipUrlSync: false, + options: [ + { + selected: false, + text: 'true', + value: 'true', + }, + { + selected: true, + text: 'false', + value: 'false', + }, + ], + query: '', + }; + const baseExpectedState = { + description: 'Switch Description', + enabledValue: 'true', + disabledValue: 'false', + hide: 0, + label: 'Switch Label', + name: 'switchVar', + skipUrlSync: false, + type: 'switch', + value: 'true', + }; + + it('should migrate a "switch" variable with "true" value', () => { + const variable: SwitchVariableModel = { + ...baseVariable, + current: { + selected: true, + text: 'true', + value: 'true', + }, + }; + + const migrated = createSceneVariableFromVariableModel(variable); + const { key, ...rest } = migrated.state; + + expect(migrated).toBeInstanceOf(SwitchVariable); + expect(rest).toEqual({ + ...baseExpectedState, + value: 'true', + }); + }); + + it('should migrate a "switch" variable with "false" value', () => { + const variable: SwitchVariableModel = { + ...baseVariable, + current: { + selected: true, + text: 'false', + value: 'false', + }, + }; + + const migrated = createSceneVariableFromVariableModel(variable); + const { key, ...rest } = migrated.state; + + expect(migrated).toBeInstanceOf(SwitchVariable); + expect(rest).toEqual({ + ...baseExpectedState, + value: 'false', + }); + }); + + it('should migrate a switch variable with array "true" value', () => { + const variable: SwitchVariableModel = { + ...baseVariable, + current: { + selected: true, + text: ['true'], + value: ['true'], + }, + }; + + const migrated = createSceneVariableFromVariableModel(variable); + const { key, ...rest } = migrated.state; + + expect(migrated).toBeInstanceOf(SwitchVariable); + expect(rest).toEqual({ + ...baseExpectedState, + value: 'true', + }); + }); + + it('should migrate a switch variable with array "false" value', () => { + const variable: SwitchVariableModel = { + ...baseVariable, + current: { + selected: true, + text: ['false'], + value: ['false'], + }, + }; + + const migrated = createSceneVariableFromVariableModel(variable); + const { key, ...rest } = migrated.state; + + expect(migrated).toBeInstanceOf(SwitchVariable); + expect(rest).toEqual({ + ...baseExpectedState, + value: 'false', + }); + }); + + it('should migrate a "switch" variable with a custom value', () => { + const variable: SwitchVariableModel = { + ...baseVariable, + current: { + selected: true, + text: 'on', + value: 'on', + }, + options: [ + { + selected: true, + text: 'on', + value: 'on', + }, + { + selected: false, + text: 'off', + value: 'off', + }, + ], + }; + + const migrated = createSceneVariableFromVariableModel(variable); + const { key, ...rest } = migrated.state; + + expect(migrated).toBeInstanceOf(SwitchVariable); + expect(rest).toEqual({ + ...baseExpectedState, + disabledValue: 'off', + enabledValue: 'on', + value: 'on', + }); + }); + }); + it.each(['system'])('should throw for unsupported (yet) variables', (type) => { const variable = { name: 'query0', diff --git a/public/app/features/dashboard-scene/utils/variables.ts b/public/app/features/dashboard-scene/utils/variables.ts index 43214dce91c..4f4ef802806 100644 --- a/public/app/features/dashboard-scene/utils/variables.ts +++ b/public/app/features/dashboard-scene/utils/variables.ts @@ -11,6 +11,7 @@ import { SceneVariable, SceneVariableSet, ScopesVariable, + SwitchVariable, TextBoxVariable, } from '@grafana/scenes'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; @@ -156,6 +157,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode ), }); } + // Custom variable if (variable.type === 'custom') { return new CustomVariable({ ...commonProperties, @@ -171,6 +173,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode hide: variable.hide, allowCustomValue: variable.allowCustomValue, }); + // Query variable } else if (variable.type === 'query') { return new QueryVariable({ ...commonProperties, @@ -196,6 +199,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode })), staticOptionsOrder: variable.staticOptionsOrder, }); + // Datasource variable } else if (variable.type === 'datasource') { return new DataSourceVariable({ ...commonProperties, @@ -212,6 +216,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode defaultOptionEnabled: variable.current?.value === DEFAULT_DATASOURCE && variable.current?.text === 'default', allowCustomValue: variable.allowCustomValue, }); + // Interval variable } else if (variable.type === 'interval') { const intervals = getIntervalsFromQueryString(variable.query); const currentInterval = getCurrentValueForOldIntervalModel(variable, intervals); @@ -226,6 +231,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode skipUrlSync: variable.skipUrlSync, hide: variable.hide, }); + // Constant variable } else if (variable.type === 'constant') { return new ConstantVariable({ ...commonProperties, @@ -233,6 +239,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode skipUrlSync: variable.skipUrlSync, hide: variable.hide, }); + // Textbox variable } else if (variable.type === 'textbox') { let val; if (!variable?.current?.value) { @@ -251,6 +258,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode skipUrlSync: variable.skipUrlSync, hide: variable.hide, }); + // Groupby variable } else if (config.featureToggles.groupByVariable && variable.type === 'groupby') { return new GroupByVariable({ ...commonProperties, @@ -264,6 +272,25 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode defaultValue: variable.defaultValue, allowCustomValue: variable.allowCustomValue, }); + // Switch variable + // In the old variable model we are storing the enabled and disabled values in the options: + // the first option is the enabled value and the second is the disabled value + } else if (variable.type === 'switch') { + const pickFirstValue = (value: string | string[]) => { + if (Array.isArray(value)) { + return value[0]; + } + return value; + }; + + return new SwitchVariable({ + ...commonProperties, + value: pickFirstValue(variable.current?.value), + enabledValue: pickFirstValue(variable.options?.[0]?.value), + disabledValue: pickFirstValue(variable.options?.[1]?.value), + skipUrlSync: variable.skipUrlSync, + hide: variable.hide, + }); } else { throw new Error(`Scenes: Unsupported variable type ${variable.type}`); } diff --git a/public/app/features/dashboard-scene/v2schema/test-helpers.ts b/public/app/features/dashboard-scene/v2schema/test-helpers.ts index b26066d1ea3..a8f2c64e346 100644 --- a/public/app/features/dashboard-scene/v2schema/test-helpers.ts +++ b/public/app/features/dashboard-scene/v2schema/test-helpers.ts @@ -8,6 +8,7 @@ import { SceneQueryRunner, SceneVariable, SceneVariableState, + SwitchVariable, VizPanel, } from '@grafana/scenes'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; @@ -43,7 +44,12 @@ export function validateVariable< ); expect(sceneVariable?.state.datasource?.type).toEqual(variableKind.group); expect(sceneVariable?.state.datasource?.uid).toEqual(variableKind.datasource?.name); - } else if (variableKind.kind !== 'AdhocVariable') { + } else if (variableKind.kind === 'SwitchVariable' && sceneVariable instanceof SwitchVariable) { + expect(sceneVariable).toBeInstanceOf(sceneVariableClass); + expect(scene.state?.$variables?.getByName(dashSpec.variables[index].spec.name)?.getValue()).toBe( + variableKind.spec.current + ); + } else if (variableKind.kind !== 'AdhocVariable' && variableKind.kind !== 'SwitchVariable') { expect(sceneVariable).toBeInstanceOf(sceneVariableClass); expect(scene.state?.$variables?.getByName(dashSpec.variables[index].spec.name)?.getValue()).toBe( variableKind.spec.current.value diff --git a/public/app/features/variables/guard.test.ts b/public/app/features/variables/guard.test.ts index 7e57f8422ad..d2094d74f4b 100644 --- a/public/app/features/variables/guard.test.ts +++ b/public/app/features/variables/guard.test.ts @@ -24,6 +24,7 @@ import { createOrgVariable, createQueryVariable, createSnapshotVariable, + createSwitchVariable, createTextBoxVariable, createUserVariable, } from './state/__tests__/fixtures'; @@ -177,6 +178,7 @@ describe('type guards', () => { dashboard: { variable: createDashboardVariable(), isMulti: false, hasOptions: false, hasCurrent: true }, custom: { variable: createCustomVariable(), isMulti: true, hasOptions: true, hasCurrent: true }, snapshot: { variable: createSnapshotVariable(), isMulti: false, hasOptions: true, hasCurrent: true }, + switch: { variable: createSwitchVariable(), isMulti: false, hasOptions: true, hasCurrent: true }, }; const variableFacts = Object.values(variableFactsObj); diff --git a/public/app/features/variables/state/__tests__/fixtures.ts b/public/app/features/variables/state/__tests__/fixtures.ts index f72326cd79b..ccd86d56ede 100644 --- a/public/app/features/variables/state/__tests__/fixtures.ts +++ b/public/app/features/variables/state/__tests__/fixtures.ts @@ -11,6 +11,7 @@ import { OrgVariableModel, QueryVariableModel, SnapshotVariableModel, + SwitchVariableModel, TextBoxVariableModel, UserVariableModel, VariableHide, @@ -209,3 +210,28 @@ export function createSnapshotVariable(input: Partial = { ...input, }; } + +export function createSwitchVariable(input: Partial = {}): SwitchVariableModel { + return { + ...createBaseVariableModel('switch'), + current: { + value: 'true', + text: 'true', + selected: true, + }, + options: [ + { + value: 'true', + text: 'true', + selected: true, + }, + { + value: 'false', + text: 'false', + selected: false, + }, + ], + query: '', + ...input, + }; +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 8ed054f3908..009c3daf324 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -5819,6 +5819,7 @@ "dynamically-switch-source-multiple-panels": "Dynamically switch the data source for multiple panels", "group": "Add keys to group by on the fly", "hidden-constant-variable": "A hidden constant variable, useful for metric prefixes in dashboards you want to share", + "users-enter-arbitrary-strings-switch": "A variable that can be toggled on and off", "users-enter-arbitrary-strings-textbox": "Users can enter any arbitrary strings in a textbox", "values-are-static-and-defined-manually": "Values are static and defined manually", "values-fetched-source-query": "Values are fetched from a data source query", @@ -5832,6 +5833,7 @@ "group-by": "Group by", "interval": "Interval", "query": "Query", + "switch": "Switch", "textbox": "Textbox" } }, @@ -6176,6 +6178,17 @@ "copy-to-clipboard-failed": "Copy to clipboard failed" } }, + "switch-variable-form": { + "disabled-value": "Disabled value", + "disabled-value-description": "Value when switch is disabled", + "disabled-value-placeholder": "e.g. Off, Disabled, Inactive", + "enabled-value": "Enabled value", + "enabled-value-description": "Value when switch is enabled", + "enabled-value-placeholder": "e.g. On, Enabled, Active", + "switch-options": "Switch options", + "value-pair-type": "Value pair type", + "value-pair-type-description": "Choose the type of values for the switch states" + }, "text-box-variable": { "name-default-value": "Default value" }, diff --git a/yarn.lock b/yarn.lock index af206586db1..4aad46d0157 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3524,11 +3524,11 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes-react@npm:6.38.0": - version: 6.38.0 - resolution: "@grafana/scenes-react@npm:6.38.0" +"@grafana/scenes-react@npm:6.39.3": + version: 6.39.3 + resolution: "@grafana/scenes-react@npm:6.39.3" dependencies: - "@grafana/scenes": "npm:6.38.0" + "@grafana/scenes": "npm:6.39.3" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3540,13 +3540,13 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/7b38553f1950f6592eb24878061cc7c45fc2aa1776c4b1cd521c6995d11f8beb0b2b275afda0ebf0ec19b7f7c79b77126caad4843bc9a75bd09881645e882c91 + checksum: 10/7e989c0d34ab23add873fcdf8f2ebf2bb94c7cfd427631be13da4a08a0f78832c354868d97e584534a69a4bf4224fcb49a8d49fb16bf43cbed58203bd36c90d4 languageName: node linkType: hard -"@grafana/scenes@npm:6.38.0": - version: 6.38.0 - resolution: "@grafana/scenes@npm:6.38.0" +"@grafana/scenes@npm:6.39.3": + version: 6.39.3 + resolution: "@grafana/scenes@npm:6.39.3" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3566,7 +3566,7 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/9372896e76271becb80e4a8691d8c51bc3b54b54d98c82f2953134f407a7370dc66a3e8afaa508003147e2f1b5b08d0ddbb6b1616a3a3ab3f7ad69fb941dd998 + checksum: 10/e5ddaf4f5e9afc7370eb4b6608d26056d1e4072465c2179e87415ac4ad01a4f8693bafaa372d7ed8ec6f295847561116ea85993cb5e5e6c4927fbb82b56f2e96 languageName: node linkType: hard @@ -18289,8 +18289,8 @@ __metadata: "@grafana/plugin-ui": "npm:^0.10.10" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" - "@grafana/scenes": "npm:6.38.0" - "@grafana/scenes-react": "npm:6.38.0" + "@grafana/scenes": "npm:6.39.3" + "@grafana/scenes-react": "npm:6.39.3" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/test-utils": "workspace:*" From 7ceb4b90f67f58bc93309adfd06a53a3eb6c097c Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 9 Oct 2025 15:38:52 +0300 Subject: [PATCH 093/578] Chore: Remove library panel thema (#112154) --- .github/CODEOWNERS | 1 - kinds/librarypanel/librarypanel_kind.cue | 63 ---------------- packages/grafana-schema/src/index.gen.ts | 17 ----- packages/grafana-schema/src/index.ts | 1 + .../librarypanel/x/librarypanel_types.gen.ts | 65 ---------------- .../src/veneer/librarypanel.types.ts | 54 +++++++++++++- pkg/kinds/librarypanel/librarypanel_gen.go | 43 ----------- .../librarypanel/librarypanel_metadata_gen.go | 42 ----------- .../librarypanel/librarypanel_spec_gen.go | 74 ------------------- .../librarypanel/librarypanel_status_gen.go | 74 ------------------- .../publicdashboard/publicdashboard_gen.go | 43 ----------- .../publicdashboard_metadata_gen.go | 42 ----------- .../publicdashboard_spec_gen.go | 32 -------- .../publicdashboard_status_gen.go | 74 ------------------- pkg/registry/schemas/core_kind.go | 9 --- pkg/services/libraryelements/api.go | 18 ++--- pkg/services/libraryelements/database.go | 21 +++--- .../libraryelements_create_test.go | 13 ++-- .../libraryelements_get_all_test.go | 65 ++++++++-------- .../libraryelements_get_test.go | 9 +-- .../libraryelements_patch_test.go | 12 ++- .../libraryelements/libraryelements_test.go | 3 +- pkg/services/libraryelements/model/model.go | 26 ++++--- .../librarypanels/librarypanels_test.go | 9 +-- .../panel-edit/PanelEditor.tsx | 2 +- .../scene/AddLibraryPanelDrawer.test.tsx | 2 +- .../variables/VariablesUnknownTable.tsx | 2 +- .../sharing/ExportButton/ResourceExport.tsx | 2 +- .../sharing/ShareExportTab.test.tsx | 2 +- .../sharing/ShareExportTab.tsx | 2 +- .../sharing/ShareLibraryPanelTab.tsx | 2 +- .../sharing/ShareSnapshotTab.tsx | 2 +- .../dashboard/components/ShareModal/types.ts | 2 +- .../AddLibraryPanelModal.tsx | 2 +- public/app/features/library-panels/types.ts | 3 +- 35 files changed, 149 insertions(+), 684 deletions(-) delete mode 100644 kinds/librarypanel/librarypanel_kind.cue delete mode 100644 packages/grafana-schema/src/raw/librarypanel/x/librarypanel_types.gen.ts delete mode 100644 pkg/kinds/librarypanel/librarypanel_gen.go delete mode 100644 pkg/kinds/librarypanel/librarypanel_metadata_gen.go delete mode 100644 pkg/kinds/librarypanel/librarypanel_spec_gen.go delete mode 100644 pkg/kinds/librarypanel/librarypanel_status_gen.go delete mode 100644 pkg/kinds/publicdashboard/publicdashboard_gen.go delete mode 100644 pkg/kinds/publicdashboard/publicdashboard_metadata_gen.go delete mode 100644 pkg/kinds/publicdashboard/publicdashboard_spec_gen.go delete mode 100644 pkg/kinds/publicdashboard/publicdashboard_status_gen.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 708740f5ce6..2af83368859 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -688,7 +688,6 @@ /packages/grafana-schema/src/**/grafanapyroscope @grafana/observability-traces-and-profiling /packages/grafana-schema/src/**/heatmap @grafana/dataviz-squad /packages/grafana-schema/src/**/histogram @grafana/dataviz-squad -/packages/grafana-schema/src/**/librarypanel @grafana/sharing-squad /packages/grafana-schema/src/**/logs @grafana/observability-logs /packages/grafana-schema/src/**/logsnew @grafana/observability-logs /packages/grafana-schema/src/**/loki @grafana/oss-big-tent @grafana/observability-logs diff --git a/kinds/librarypanel/librarypanel_kind.cue b/kinds/librarypanel/librarypanel_kind.cue deleted file mode 100644 index b87ceccb6fc..00000000000 --- a/kinds/librarypanel/librarypanel_kind.cue +++ /dev/null @@ -1,63 +0,0 @@ -package kind - -import ( - "strings" - "time" -) - -name: "LibraryPanel" -maturity: "experimental" -description: "A standalone panel" - -lineage: schemas: [{ - version: [0, 0] - schema: { - spec: { - // Folder UID - folderUid?: string @grafanamaturity(ToMetadata="sys") - - // Library element UID - uid: string - - // Panel name (also saved in the model) - name: string & strings.MinRunes(1) - - // Panel description - description?: string - - // The panel type (from inside the model) - type: string & strings.MinRunes(1) - - // Dashboard version when this was saved (zero if unknown) - schemaVersion?: uint16 - - // panel version, incremented each time the dashboard is updated. - version: int64 @grafanamaturity(NeedsExpertReview) - - // TODO: should be the same panel schema defined in dashboard - // Typescript: Omit; - model: {...} - - // Object storage metadata - meta?: #LibraryElementDTOMeta @grafanamaturity(ToMetadata="sys") - } @cuetsy(kind="interface") @grafana(TSVeneer="type") - - #LibraryElementDTOMetaUser: { - id: int64 - name: string - avatarUrl: string - } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) - - #LibraryElementDTOMeta: { - folderName: string - folderUid: string @grafanamaturity(ToMetadata="sys") - connectedDashboards: int64 - - created: string & time.Time - updated: string & time.Time - - createdBy: #LibraryElementDTOMetaUser @grafanamaturity(ToMetadata="sys") - updatedBy: #LibraryElementDTOMetaUser @grafanamaturity(ToMetadata="sys") - } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) - } -}] diff --git a/packages/grafana-schema/src/index.gen.ts b/packages/grafana-schema/src/index.gen.ts index 5646058691b..87f2a3fd960 100644 --- a/packages/grafana-schema/src/index.gen.ts +++ b/packages/grafana-schema/src/index.gen.ts @@ -104,20 +104,3 @@ export { defaultFieldConfig, defaultRowPanel } from './veneer/dashboard.types'; - -// Raw generated types from LibraryPanel kind. -export type { - LibraryElementDTOMetaUser, - LibraryElementDTOMeta -} from './raw/librarypanel/x/librarypanel_types.gen'; - -// The following exported declarations correspond to types in the librarypanel@0.0 kind's -// schema with attribute @grafana(TSVeneer="type"). -// -// The handwritten file for these type and default veneers is expected to be at -// packages/grafana-schema/src/veneer/librarypanel.types.ts. -// This re-export declaration enforces that the handwritten veneer file exists, -// and exports all the symbols in the list. -// -// TODO generate code such that tsc enforces type compatibility between raw and veneer decls -export type { LibraryPanel } from './veneer/librarypanel.types'; diff --git a/packages/grafana-schema/src/index.ts b/packages/grafana-schema/src/index.ts index 505aded6a29..e7588ef48a1 100644 --- a/packages/grafana-schema/src/index.ts +++ b/packages/grafana-schema/src/index.ts @@ -4,4 +4,5 @@ * @packageDocumentation */ export * from './veneer/common.types'; +export * from './veneer/librarypanel.types'; export * from './index.gen'; 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 deleted file mode 100644 index 97cba2a5c5e..00000000000 --- a/packages/grafana-schema/src/raw/librarypanel/x/librarypanel_types.gen.ts +++ /dev/null @@ -1,65 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// TSTypesJenny -// LatestMajorsOrXJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -export interface LibraryElementDTOMetaUser { - avatarUrl: string; - id: number; - name: string; -} - -export interface LibraryElementDTOMeta { - connectedDashboards: number; - created: string; - createdBy: LibraryElementDTOMetaUser; - folderName: string; - folderUid: string; - updated: string; - updatedBy: LibraryElementDTOMetaUser; -} - -export interface LibraryPanel { - /** - * Panel description - */ - description?: string; - /** - * Folder UID - */ - folderUid?: string; - /** - * Object storage metadata - */ - meta?: LibraryElementDTOMeta; - /** - * TODO: should be the same panel schema defined in dashboard - * Typescript: Omit; - */ - model: Record; - /** - * Panel name (also saved in the model) - */ - name: string; - /** - * Dashboard version when this was saved (zero if unknown) - */ - schemaVersion?: number; - /** - * The panel type (from inside the model) - */ - type: string; - /** - * Library element UID - */ - uid: string; - /** - * panel version, incremented each time the dashboard is updated. - */ - version: number; -} diff --git a/packages/grafana-schema/src/veneer/librarypanel.types.ts b/packages/grafana-schema/src/veneer/librarypanel.types.ts index 007bb085742..0065ebe25e2 100644 --- a/packages/grafana-schema/src/veneer/librarypanel.types.ts +++ b/packages/grafana-schema/src/veneer/librarypanel.types.ts @@ -1,7 +1,55 @@ -import * as raw from '../raw/librarypanel/x/librarypanel_types.gen'; - import { Panel } from './dashboard.types'; -export interface LibraryPanel extends raw.LibraryPanel { +export interface LibraryElementDTOMetaUser { + avatarUrl: string; + id: number; + name: string; +} + +export interface LibraryElementDTOMeta { + connectedDashboards: number; + created: string; + createdBy: LibraryElementDTOMetaUser; + folderName: string; + folderUid: string; + updated: string; + updatedBy: LibraryElementDTOMetaUser; +} + +export interface LibraryPanel { + /** + * Panel description + */ + description?: string; + /** + * Folder UID + */ + folderUid?: string; + /** + * Object storage metadata + */ + meta?: LibraryElementDTOMeta; + + // The panel model: Omit; + /** + * Panel name (also saved in the model) + */ + name: string; + /** + * Dashboard version when this was saved (zero if unknown) + */ + schemaVersion?: number; + /** + * The panel type (from inside the model) + */ + type: string; + /** + * Library element UID + */ + uid: string; + /** + * panel version, incremented each time the dashboard is updated. + */ + version: number; } diff --git a/pkg/kinds/librarypanel/librarypanel_gen.go b/pkg/kinds/librarypanel/librarypanel_gen.go deleted file mode 100644 index ea25be0ad4f..00000000000 --- a/pkg/kinds/librarypanel/librarypanel_gen.go +++ /dev/null @@ -1,43 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package librarypanel - -import ( - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/grafana/grafana/pkg/kinds" -) - -// Resource is the kubernetes style representation of LibraryPanel. (TODO be better) -type K8sResource = kinds.GrafanaResource[Spec, Status] - -// NewResource creates a new instance of the resource with a given name (UID) -func NewK8sResource(name string, s *Spec) K8sResource { - return K8sResource{ - TypeMeta: v1.TypeMeta{ - Kind: "LibraryPanel", - APIVersion: "v0-0-alpha", - }, - ObjectMeta: v1.ObjectMeta{ - Name: name, - Annotations: make(map[string]string), - Labels: make(map[string]string), - }, - Spec: s, - } -} - -// Resource is the wire representation of LibraryPanel. -// It currently will soon be merged into the k8s flavor (TODO be better) -type Resource struct { - Metadata Metadata `json:"metadata"` - Spec Spec `json:"spec"` - Status Status `json:"status"` -} diff --git a/pkg/kinds/librarypanel/librarypanel_metadata_gen.go b/pkg/kinds/librarypanel/librarypanel_metadata_gen.go deleted file mode 100644 index e899f28b1ff..00000000000 --- a/pkg/kinds/librarypanel/librarypanel_metadata_gen.go +++ /dev/null @@ -1,42 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package librarypanel - -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/kinds/librarypanel/librarypanel_spec_gen.go b/pkg/kinds/librarypanel/librarypanel_spec_gen.go deleted file mode 100644 index cb5daf921a2..00000000000 --- a/pkg/kinds/librarypanel/librarypanel_spec_gen.go +++ /dev/null @@ -1,74 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// GoResourceTypes -// -// Run 'make gen-cue' from repository root to regenerate. - -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package librarypanel - -import ( - time "time" -) - -type Spec struct { - // Folder UID - FolderUid *string `json:"folderUid,omitempty"` - // Library element UID - Uid string `json:"uid"` - // Panel name (also saved in the model) - Name string `json:"name"` - // Panel description - Description *string `json:"description,omitempty"` - // The panel type (from inside the model) - Type string `json:"type"` - // Dashboard version when this was saved (zero if unknown) - SchemaVersion *uint16 `json:"schemaVersion,omitempty"` - // panel version, incremented each time the dashboard is updated. - Version int64 `json:"version"` - // TODO: should be the same panel schema defined in dashboard - // Typescript: Omit; - Model map[string]any `json:"model"` - // Object storage metadata - Meta *LibraryElementDTOMeta `json:"meta,omitempty"` -} - -// NewSpec creates a new Spec object. -func NewSpec() *Spec { - return &Spec{ - Model: map[string]any{}, - } -} - -type LibraryElementDTOMeta struct { - FolderName string `json:"folderName"` - FolderUid string `json:"folderUid"` - ConnectedDashboards int64 `json:"connectedDashboards"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - CreatedBy LibraryElementDTOMetaUser `json:"createdBy"` - UpdatedBy LibraryElementDTOMetaUser `json:"updatedBy"` -} - -// NewLibraryElementDTOMeta creates a new LibraryElementDTOMeta object. -func NewLibraryElementDTOMeta() *LibraryElementDTOMeta { - return &LibraryElementDTOMeta{ - CreatedBy: *NewLibraryElementDTOMetaUser(), - UpdatedBy: *NewLibraryElementDTOMetaUser(), - } -} - -type LibraryElementDTOMetaUser struct { - Id int64 `json:"id"` - Name string `json:"name"` - AvatarUrl string `json:"avatarUrl"` -} - -// NewLibraryElementDTOMetaUser creates a new LibraryElementDTOMetaUser object. -func NewLibraryElementDTOMetaUser() *LibraryElementDTOMetaUser { - return &LibraryElementDTOMetaUser{} -} diff --git a/pkg/kinds/librarypanel/librarypanel_status_gen.go b/pkg/kinds/librarypanel/librarypanel_status_gen.go deleted file mode 100644 index 69072c08dff..00000000000 --- a/pkg/kinds/librarypanel/librarypanel_status_gen.go +++ /dev/null @@ -1,74 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package librarypanel - -// 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/publicdashboard/publicdashboard_gen.go b/pkg/kinds/publicdashboard/publicdashboard_gen.go deleted file mode 100644 index 68239e64690..00000000000 --- a/pkg/kinds/publicdashboard/publicdashboard_gen.go +++ /dev/null @@ -1,43 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package publicdashboard - -import ( - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/grafana/grafana/pkg/kinds" -) - -// Resource is the kubernetes style representation of PublicDashboard. (TODO be better) -type K8sResource = kinds.GrafanaResource[Spec, Status] - -// NewResource creates a new instance of the resource with a given name (UID) -func NewK8sResource(name string, s *Spec) K8sResource { - return K8sResource{ - TypeMeta: v1.TypeMeta{ - Kind: "PublicDashboard", - APIVersion: "v0-0-alpha", - }, - ObjectMeta: v1.ObjectMeta{ - Name: name, - Annotations: make(map[string]string), - Labels: make(map[string]string), - }, - Spec: s, - } -} - -// Resource is the wire representation of PublicDashboard. -// It currently will soon be merged into the k8s flavor (TODO be better) -type Resource struct { - Metadata Metadata `json:"metadata"` - Spec Spec `json:"spec"` - Status Status `json:"status"` -} diff --git a/pkg/kinds/publicdashboard/publicdashboard_metadata_gen.go b/pkg/kinds/publicdashboard/publicdashboard_metadata_gen.go deleted file mode 100644 index c5b84bf2700..00000000000 --- a/pkg/kinds/publicdashboard/publicdashboard_metadata_gen.go +++ /dev/null @@ -1,42 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package publicdashboard - -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/kinds/publicdashboard/publicdashboard_spec_gen.go b/pkg/kinds/publicdashboard/publicdashboard_spec_gen.go deleted file mode 100644 index 667cf38cff1..00000000000 --- a/pkg/kinds/publicdashboard/publicdashboard_spec_gen.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// GoResourceTypes -// -// Run 'make gen-cue' from repository root to regenerate. - -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package publicdashboard - -type Spec struct { - // Unique public dashboard identifier - Uid string `json:"uid"` - // Dashboard unique identifier referenced by this public dashboard - DashboardUid string `json:"dashboardUid"` - // Unique public access token - AccessToken *string `json:"accessToken,omitempty"` - // Flag that indicates if the public dashboard is enabled - IsEnabled bool `json:"isEnabled"` - // Flag that indicates if annotations are enabled - AnnotationsEnabled bool `json:"annotationsEnabled"` - // Flag that indicates if the time range picker is enabled - TimeSelectionEnabled bool `json:"timeSelectionEnabled"` -} - -// NewSpec creates a new Spec object. -func NewSpec() *Spec { - return &Spec{} -} diff --git a/pkg/kinds/publicdashboard/publicdashboard_status_gen.go b/pkg/kinds/publicdashboard/publicdashboard_status_gen.go deleted file mode 100644 index 95de4433cfe..00000000000 --- a/pkg/kinds/publicdashboard/publicdashboard_status_gen.go +++ /dev/null @@ -1,74 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// kinds/gen.go -// Using jennies: -// K8ResourcesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package publicdashboard - -// 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/registry/schemas/core_kind.go b/pkg/registry/schemas/core_kind.go index c60b38d6161..1c8a9e37802 100644 --- a/pkg/registry/schemas/core_kind.go +++ b/pkg/registry/schemas/core_kind.go @@ -39,15 +39,6 @@ func GetCoreKinds() ([]CoreKind, error) { CueFile: dashboardCue, }) - librarypanelCue, err := loadCueFile(ctx, filepath.Join(root, "./kinds/librarypanel/librarypanel_kind.cue")) - if err != nil { - return nil, err - } - kinds = append(kinds, CoreKind{ - Name: "librarypanel", - CueFile: librarypanelCue, - }) - return kinds, nil } diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go index 55ad2c01082..791cef618a9 100644 --- a/pkg/services/libraryelements/api.go +++ b/pkg/services/libraryelements/api.go @@ -8,13 +8,18 @@ import ( "net/http" "strings" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/kinds/librarypanel" ac "github.com/grafana/grafana/pkg/services/accesscontrol" grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -28,11 +33,6 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/errhttp" "github.com/grafana/grafana/pkg/web" - k8serrors "k8s.io/apimachinery/pkg/api/errors" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" ) func (l *LibraryElementService) registerAPIEndpoints() { @@ -340,7 +340,7 @@ func (l *LibraryElementService) getConnectionsHandler(c *contextmodel.ReqContext ConnectionID: dashboard.ID, // nolint:staticcheck ConnectionUID: dashboard.UID, // returns the creation information of the library element, not the connection - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: element.Meta.CreatedBy.Id, Name: element.Meta.CreatedBy.Name, AvatarUrl: element.Meta.CreatedBy.AvatarUrl, @@ -713,7 +713,7 @@ func (lk8s *libraryElementsK8sHandler) unstructuredToLegacyLibraryPanelDTO(c *co } for _, user := range users { if user.UID == createdBy { - dto.Meta.CreatedBy = librarypanel.LibraryElementDTOMetaUser{ + dto.Meta.CreatedBy = model.LibraryElementDTOMetaUser{ Id: user.ID, Name: user.Login, AvatarUrl: dtos.GetGravatarUrl(lk8s.cfg, user.Email), @@ -721,7 +721,7 @@ func (lk8s *libraryElementsK8sHandler) unstructuredToLegacyLibraryPanelDTO(c *co } // not else because /api returns the same user for updated if it was never updated if user.UID == updatedBy { - dto.Meta.UpdatedBy = librarypanel.LibraryElementDTOMetaUser{ + dto.Meta.UpdatedBy = model.LibraryElementDTOMetaUser{ Id: user.ID, Name: user.Login, AvatarUrl: dtos.GetGravatarUrl(lk8s.cfg, user.Email), diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 82531e39077..d4923ffa20a 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/kinds/librarypanel" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -200,12 +199,12 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn ConnectedDashboards: 0, Created: element.Created, Updated: element.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: element.CreatedBy, Name: signedInUser.GetLogin(), AvatarUrl: dtos.GetGravatarUrl(l.Cfg, signedInUser.GetEmail()), }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: element.UpdatedBy, Name: signedInUser.GetLogin(), AvatarUrl: dtos.GetGravatarUrl(l.Cfg, signedInUser.GetEmail()), @@ -363,12 +362,12 @@ func (l *LibraryElementService) getLibraryElements(c context.Context, store db.D ConnectedDashboards: libraryElement.ConnectedDashboards, Created: libraryElement.Created, Updated: libraryElement.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: libraryElement.CreatedBy, Name: libraryElement.CreatedByName, AvatarUrl: dtos.GetGravatarUrl(l.Cfg, libraryElement.CreatedByEmail), }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: libraryElement.UpdatedBy, Name: libraryElement.UpdatedByName, AvatarUrl: dtos.GetGravatarUrl(l.Cfg, libraryElement.UpdatedByEmail), @@ -509,12 +508,12 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI ConnectedDashboards: element.ConnectedDashboards, Created: element.Created, Updated: element.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: element.CreatedBy, Name: element.CreatedByName, AvatarUrl: dtos.GetGravatarUrl(l.Cfg, element.CreatedByEmail), }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: element.UpdatedBy, Name: element.UpdatedByName, AvatarUrl: dtos.GetGravatarUrl(l.Cfg, element.UpdatedByEmail), @@ -670,12 +669,12 @@ func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInU ConnectedDashboards: elementInDB.ConnectedDashboards, Created: libraryElement.Created, Updated: libraryElement.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: elementInDB.CreatedBy, Name: elementInDB.CreatedByName, AvatarUrl: dtos.GetGravatarUrl(l.Cfg, elementInDB.CreatedByEmail), }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: libraryElement.UpdatedBy, Name: signedInUser.GetLogin(), AvatarUrl: dtos.GetGravatarUrl(l.Cfg, signedInUser.GetEmail()), @@ -761,12 +760,12 @@ func (l *LibraryElementService) getElementsForDashboardID(c context.Context, das ConnectedDashboards: element.ConnectedDashboards, Created: element.Created, Updated: element.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: element.CreatedBy, Name: element.CreatedByName, AvatarUrl: dtos.GetGravatarUrl(l.Cfg, element.CreatedByEmail), }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: element.UpdatedBy, Name: element.UpdatedByName, AvatarUrl: dtos.GetGravatarUrl(l.Cfg, element.UpdatedByEmail), diff --git a/pkg/services/libraryelements/libraryelements_create_test.go b/pkg/services/libraryelements/libraryelements_create_test.go index a4aea2e0241..36f2100308c 100644 --- a/pkg/services/libraryelements/libraryelements_create_test.go +++ b/pkg/services/libraryelements/libraryelements_create_test.go @@ -6,7 +6,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/testutil" @@ -51,12 +50,12 @@ func TestIntegration_CreateLibraryElement(t *testing.T) { ConnectedDashboards: 0, Created: sc.initialResult.Result.Meta.Created, Updated: sc.initialResult.Result.Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: "signed_in_user", AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: "signed_in_user", AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", @@ -102,12 +101,12 @@ func TestIntegration_CreateLibraryElement(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Meta.Created, Updated: result.Result.Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: "signed_in_user", AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: "signed_in_user", AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", @@ -181,12 +180,12 @@ func TestIntegration_CreateLibraryElement(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Meta.Created, Updated: result.Result.Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: "signed_in_user", AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: "signed_in_user", AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", diff --git a/pkg/services/libraryelements/libraryelements_get_all_test.go b/pkg/services/libraryelements/libraryelements_get_all_test.go index de58848ec15..612f5a9c279 100644 --- a/pkg/services/libraryelements/libraryelements_get_all_test.go +++ b/pkg/services/libraryelements/libraryelements_get_all_test.go @@ -7,7 +7,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/org" @@ -84,12 +83,12 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, @@ -120,12 +119,12 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[1].Meta.Created, Updated: result.Result.Elements[1].Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, @@ -187,12 +186,12 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, @@ -223,12 +222,12 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[1].Meta.Created, Updated: result.Result.Elements[1].Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, @@ -312,12 +311,12 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, @@ -348,12 +347,12 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[1].Meta.Created, Updated: result.Result.Elements[1].Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, @@ -462,12 +461,12 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, @@ -562,12 +561,12 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, @@ -598,12 +597,12 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[1].Meta.Created, Updated: result.Result.Elements[1].Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, @@ -665,12 +664,12 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, @@ -732,12 +731,12 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, @@ -800,12 +799,12 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, @@ -878,12 +877,12 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, @@ -954,12 +953,12 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, @@ -990,12 +989,12 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[1].Meta.Created, Updated: result.Result.Elements[1].Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, @@ -1059,12 +1058,12 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, diff --git a/pkg/services/libraryelements/libraryelements_get_test.go b/pkg/services/libraryelements/libraryelements_get_test.go index 69fc7d24462..c85c22ee676 100644 --- a/pkg/services/libraryelements/libraryelements_get_test.go +++ b/pkg/services/libraryelements/libraryelements_get_test.go @@ -8,7 +8,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/libraryelements/model" @@ -61,12 +60,12 @@ func TestIntegration_GetLibraryElement(t *testing.T) { ConnectedDashboards: 0, Created: res.Result.Meta.Created, Updated: res.Result.Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, @@ -161,12 +160,12 @@ func TestIntegration_GetLibraryElement(t *testing.T) { ConnectedDashboards: 1, Created: res.Result.Meta.Created, Updated: res.Result.Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, diff --git a/pkg/services/libraryelements/libraryelements_patch_test.go b/pkg/services/libraryelements/libraryelements_patch_test.go index a2bf31309f4..0cf29a83857 100644 --- a/pkg/services/libraryelements/libraryelements_patch_test.go +++ b/pkg/services/libraryelements/libraryelements_patch_test.go @@ -3,14 +3,12 @@ package libraryelements import ( "testing" - "github.com/grafana/grafana/pkg/kinds/librarypanel" - "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/services/libraryelements/model" - "github.com/grafana/grafana/pkg/util" - "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/libraryelements/model" + "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/testutil" "github.com/grafana/grafana/pkg/web" ) @@ -83,12 +81,12 @@ func TestIntegration_PatchLibraryElement(t *testing.T) { ConnectedDashboards: 0, Created: sc.initialResult.Result.Meta.Created, Updated: result.Result.Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: "signed_in_user", AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 7a4faa08fbd..e782c20ac03 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -19,7 +19,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" @@ -118,7 +117,7 @@ func TestIntegration_GetLibraryPanelConnections(t *testing.T) { ElementID: 1, ConnectionID: 1, Created: res.Result[0].Created, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, diff --git a/pkg/services/libraryelements/model/model.go b/pkg/services/libraryelements/model/model.go index 82b30fc6f60..6e2bdfdcc41 100644 --- a/pkg/services/libraryelements/model/model.go +++ b/pkg/services/libraryelements/model/model.go @@ -4,8 +4,6 @@ import ( "encoding/json" "errors" "time" - - "github.com/grafana/grafana/pkg/kinds/librarypanel" ) type LibraryConnectionKind int @@ -99,8 +97,8 @@ type LibraryElementDTOMeta struct { Created time.Time `json:"created"` Updated time.Time `json:"updated"` - CreatedBy librarypanel.LibraryElementDTOMetaUser `json:"createdBy"` - UpdatedBy librarypanel.LibraryElementDTOMetaUser `json:"updatedBy"` + CreatedBy LibraryElementDTOMetaUser `json:"createdBy"` + UpdatedBy LibraryElementDTOMetaUser `json:"updatedBy"` } // libraryElementConnection is the model for library element connections. @@ -126,16 +124,22 @@ type LibraryElementConnectionWithMeta struct { CreatedByEmail string } +type LibraryElementDTOMetaUser struct { + Id int64 `json:"id"` + Name string `json:"name"` + AvatarUrl string `json:"avatarUrl"` +} + // LibraryElementConnectionDTO is the frontend DTO for element connections. type LibraryElementConnectionDTO struct { // Deprecated: this field will be removed in the future - ID int64 `json:"id"` - Kind int64 `json:"kind"` - ElementID int64 `json:"elementId"` - ConnectionID int64 `json:"connectionId"` - ConnectionUID string `json:"connectionUid"` - Created time.Time `json:"created"` - CreatedBy librarypanel.LibraryElementDTOMetaUser `json:"createdBy"` + ID int64 `json:"id"` + Kind int64 `json:"kind"` + ElementID int64 `json:"elementId"` + ConnectionID int64 `json:"connectionId"` + ConnectionUID string `json:"connectionUid"` + Created time.Time `json:"created"` + CreatedBy LibraryElementDTOMetaUser `json:"createdBy"` } var ( diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index e47a9b6738d..a5ebd74d609 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -656,12 +655,12 @@ func toLibraryElement(t *testing.T, res model.LibraryElementDTO) libraryElement ConnectedDashboards: res.Meta.ConnectedDashboards, Created: res.Meta.Created, Updated: res.Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: res.Meta.CreatedBy.Id, Name: res.Meta.CreatedBy.Name, AvatarUrl: res.Meta.CreatedBy.AvatarUrl, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: res.Meta.UpdatedBy.Id, Name: res.Meta.UpdatedBy.Name, AvatarUrl: res.Meta.UpdatedBy.AvatarUrl, @@ -694,12 +693,12 @@ func getExpected(t *testing.T, res model.LibraryElementDTO, UID string, name str ConnectedDashboards: 0, Created: res.Meta.Created, Updated: res.Meta.Updated, - CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + CreatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, }, - UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + UpdatedBy: model.LibraryElementDTOMetaUser{ Id: 1, Name: userInDbName, AvatarUrl: userInDbAvatar, diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index fa8b5a601e8..1fe646ca69b 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -17,7 +17,7 @@ import { VizPanel, isSceneObject, } from '@grafana/scenes'; -import { Panel } from '@grafana/schema/dist/esm/index.gen'; +import { Panel } from '@grafana/schema'; import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions'; import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard'; import { saveLibPanel } from 'app/features/library-panels/state/api'; diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx index a1bbb12ac8c..06ccc67becf 100644 --- a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx @@ -1,5 +1,5 @@ import { SceneTimeRange, VizPanel } from '@grafana/scenes'; -import { LibraryPanel } from '@grafana/schema/dist/esm/index.gen'; +import { LibraryPanel } from '@grafana/schema'; import { activateFullSceneTree } from '../utils/test-utils'; diff --git a/public/app/features/dashboard-scene/settings/variables/VariablesUnknownTable.tsx b/public/app/features/dashboard-scene/settings/variables/VariablesUnknownTable.tsx index da216fd78ba..c392066b08f 100644 --- a/public/app/features/dashboard-scene/settings/variables/VariablesUnknownTable.tsx +++ b/public/app/features/dashboard-scene/settings/variables/VariablesUnknownTable.tsx @@ -6,7 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { reportInteraction } from '@grafana/runtime'; import { SceneVariable, SceneVariableState } from '@grafana/scenes'; -import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; +import { Dashboard } from '@grafana/schema'; import { CollapsableSection, Icon, Spinner, Stack, Tooltip, useStyles2 } from '@grafana/ui'; import { VariableUsagesButton } from '../../variables/VariableUsagesButton'; diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx index 6543b641927..b58bf8377af 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx @@ -2,7 +2,7 @@ import { AsyncState } from 'react-use/lib/useAsync'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; -import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; +import { Dashboard } from '@grafana/schema'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { Alert, Label, RadioButtonGroup, Stack, Switch } from '@grafana/ui'; import { DashboardJson } from 'app/features/manage-dashboards/types'; diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx index 8fa8c102ee6..1e7bee6b613 100644 --- a/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx @@ -1,6 +1,6 @@ import { config } from '@grafana/runtime'; import { SceneTimeRange } from '@grafana/scenes'; -import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; +import { Dashboard } from '@grafana/schema'; import { Spec as DashboardV2Spec, defaultQueryGroupKind, diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx index ea52c8b0da3..0e9d6ad49c5 100644 --- a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx @@ -7,7 +7,7 @@ import AutoSizer from 'react-virtualized-auto-sizer'; import { Trans, t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes'; -import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; +import { Dashboard } from '@grafana/schema'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { Button, ClipboardButton, CodeEditor, Field, Modal, Stack, Switch } from '@grafana/ui'; import { ObjectMeta } from 'app/features/apiserver/types'; diff --git a/public/app/features/dashboard-scene/sharing/ShareLibraryPanelTab.tsx b/public/app/features/dashboard-scene/sharing/ShareLibraryPanelTab.tsx index 53a0a9dc222..4c882b60618 100644 --- a/public/app/features/dashboard-scene/sharing/ShareLibraryPanelTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareLibraryPanelTab.tsx @@ -1,6 +1,6 @@ import { t } from '@grafana/i18n'; import { SceneComponentProps, SceneObjectBase, SceneObjectRef, VizPanel } from '@grafana/scenes'; -import { LibraryPanel } from '@grafana/schema/dist/esm/index.gen'; +import { LibraryPanel } from '@grafana/schema'; import { ShareLibraryPanel } from 'app/features/dashboard/components/ShareModal/ShareLibraryPanel'; import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; diff --git a/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx index e47aef533db..6c02dd93830 100644 --- a/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx @@ -5,7 +5,7 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { getBackendSrv } from '@grafana/runtime'; import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectRef, VizPanel } from '@grafana/scenes'; -import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; +import { Dashboard } from '@grafana/schema'; import { Button, ClipboardButton, Field, Input, Modal, RadioButtonGroup, Stack } from '@grafana/ui'; import { notifyApp } from 'app/core/actions'; import { createSuccessNotification } from 'app/core/copy/appNotification'; diff --git a/public/app/features/dashboard/components/ShareModal/types.ts b/public/app/features/dashboard/components/ShareModal/types.ts index 54992ec1657..0dcfa36dc1e 100644 --- a/public/app/features/dashboard/components/ShareModal/types.ts +++ b/public/app/features/dashboard/components/ShareModal/types.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { NavModelItem } from '@grafana/data'; -import { LibraryPanel } from '@grafana/schema/dist/esm/index.gen'; +import { LibraryPanel } from '@grafana/schema'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; diff --git a/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx b/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx index 753b01e8e15..57de268b917 100644 --- a/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx +++ b/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx @@ -3,7 +3,7 @@ import { useAsync, useDebounce } from 'react-use'; import { Trans, t } from '@grafana/i18n'; import { FetchError, isFetchError } from '@grafana/runtime'; -import { LibraryPanel } from '@grafana/schema/dist/esm/index.gen'; +import { LibraryPanel } from '@grafana/schema'; import { Button, Field, Input, Modal, Stack } from '@grafana/ui'; import { FolderPicker } from 'app/core/components/Select/FolderPicker'; diff --git a/public/app/features/library-panels/types.ts b/public/app/features/library-panels/types.ts index 5ce33b72620..5fc3883dba8 100644 --- a/public/app/features/library-panels/types.ts +++ b/public/app/features/library-panels/types.ts @@ -1,8 +1,7 @@ import { AnyAction } from '@reduxjs/toolkit'; import { Dispatch } from 'react'; -import { LibraryPanel } from '@grafana/schema'; -import { LibraryElementDTOMetaUser } from '@grafana/schema/src/raw/librarypanel/x/librarypanel_types.gen'; +import { LibraryPanel, LibraryElementDTOMetaUser } from '@grafana/schema'; import { PanelModel } from '../dashboard/state/PanelModel'; From 66dddb415e2b7eb89b9ba6e2a9594e4a9277decd Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Thu, 9 Oct 2025 15:20:02 +0200 Subject: [PATCH 094/578] Restore dashboards: Update delete modal text (#112148) * Restore dashboards: Update delete modal text * Update subheader --- public/app/core/utils/navBarItem-translations.ts | 2 +- .../components/BrowseActions/DeleteModal.tsx | 7 ++++--- public/locales/en-US/grafana.json | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/public/app/core/utils/navBarItem-translations.ts b/public/app/core/utils/navBarItem-translations.ts index 80f1d36aa34..0fab359de4e 100644 --- a/public/app/core/utils/navBarItem-translations.ts +++ b/public/app/core/utils/navBarItem-translations.ts @@ -220,7 +220,7 @@ export function getNavSubTitle(navId: string | undefined) { case 'dashboards/recently-deleted': return t( 'nav.recently-deleted.subtitle', - 'Any items listed here for more than 30 days will be automatically deleted.' + 'Deleted dashboards are kept for up to 12 months or until the history limit of 1000 dashboards is reached.' ); case 'alerting': return t('nav.alerting.subtitle', 'Learn about problems in your systems moments after they occur'); diff --git a/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx b/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx index bcd489477ad..be238108e7f 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx +++ b/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx @@ -48,9 +48,10 @@ export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: P <> - This action will delete the selected folders immediately but the selected dashboards will be marked - for deletion in 30 days. Your organization administrator can restore the dashboards anytime before the - 30 days expire. Folders cannot be restored. + This action will delete the selected folders immediately. Deleted dashboards will be kept in the + history for up to 12 months and can be restored by your organization administrator during that time. + The history is limited to 1000 dashboards — older ones may be removed sooner if the limit is reached. + Folders cannot be restored. diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 009c3daf324..5f0f34d651d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -3504,7 +3504,7 @@ "delete-button": "Delete", "delete-modal-invalid-text": "One or more folders contain library panels or alert rules. Delete these first in order to proceed.", "delete-modal-invalid-title": "Cannot delete folder", - "delete-modal-restore-dashboards-text": "This action will delete the selected folders immediately but the selected dashboards will be marked for deletion in 30 days. Your organization administrator can restore the dashboards anytime before the 30 days expire. Folders cannot be restored.", + "delete-modal-restore-dashboards-text": "This action will delete the selected folders immediately. Deleted dashboards will be kept in the history for up to 12 months and can be restored by your organization administrator during that time. The history is limited to 1000 dashboards — older ones may be removed sooner if the limit is reached. Folders cannot be restored.", "delete-modal-text": "This action will delete the following content:", "delete-modal-title": "Delete", "delete-provisioned-folder": "Delete provisioned folder", @@ -10368,7 +10368,7 @@ "title": "Profiles" }, "recently-deleted": { - "subtitle": "Any items listed here for more than 30 days will be automatically deleted.", + "subtitle": "Deleted dashboards are kept for up to 12 months or until the history limit of 1000 dashboards is reached.", "title": "Recently deleted" }, "recorded-queries": { From caaccb7984df6590fd340445250665d7e9c08e27 Mon Sep 17 00:00:00 2001 From: Yunwen Zheng Date: Thu, 9 Oct 2025 09:20:29 -0400 Subject: [PATCH 095/578] RepositoryOverview: Adjust layout and extract necessary components out (#112190) * RepositoryOverview: adjust layout and extract necessary components out * clean up * clean up --- .../features/provisioning/Job/RecentJobs.tsx | 25 +-- .../Repository/RepositoryHealth.tsx | 42 ---- .../Repository/RepositoryHealthCard.tsx | 88 +++++++++ .../Repository/RepositoryOverview.tsx | 180 ++++-------------- .../Repository/RepositoryPullStatusCard.tsx | 99 ++++++++++ public/app/features/provisioning/utils/git.ts | 37 ++++ .../provisioning/utils/repositoryStatus.ts | 42 ++++ public/locales/en-US/grafana.json | 10 +- 8 files changed, 317 insertions(+), 206 deletions(-) delete mode 100644 public/app/features/provisioning/Repository/RepositoryHealth.tsx create mode 100644 public/app/features/provisioning/Repository/RepositoryHealthCard.tsx create mode 100644 public/app/features/provisioning/Repository/RepositoryPullStatusCard.tsx create mode 100644 public/app/features/provisioning/utils/repositoryStatus.ts diff --git a/public/app/features/provisioning/Job/RecentJobs.tsx b/public/app/features/provisioning/Job/RecentJobs.tsx index e6265d804b0..660851f7003 100644 --- a/public/app/features/provisioning/Job/RecentJobs.tsx +++ b/public/app/features/provisioning/Job/RecentJobs.tsx @@ -3,11 +3,12 @@ import { useMemo } from 'react'; import { intervalToAbbreviatedDurationString, TraceKeyValuePair } from '@grafana/data'; import { t, Trans } from '@grafana/i18n'; import { Alert, Badge, Box, Card, InteractiveTable, Spinner, Stack, Text } from '@grafana/ui'; -import { Job, Repository, SyncStatus } from 'app/api/clients/provisioning/v0alpha1'; +import { Job, Repository } from 'app/api/clients/provisioning/v0alpha1'; import KeyValuesTable from 'app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable'; import { ProvisioningAlert } from '../Shared/ProvisioningAlert'; import { useRepositoryAllJobs } from '../hooks/useRepositoryAllJobs'; +import { getStatusColor } from '../utils/repositoryStatus'; import { formatTimestamp } from '../utils/time'; import { JobSummary } from './JobSummary'; @@ -22,24 +23,12 @@ type JobCell = { }; }; -const getStatusColor = (state?: SyncStatus['state']) => { - switch (state) { - case 'success': - return 'green'; - case 'working': - return 'blue'; - case 'warning': - return 'orange'; - case 'pending': - return 'darkgrey'; - case 'error': - return 'red'; - default: - return 'darkgrey'; - } -}; - const getJobColumns = () => [ + { + id: 'jobId', + header: t('provisioning.recent-jobs.column-job-id', 'Job ID'), + cell: ({ row: { original: job } }: JobCell) => {job.metadata?.name || ''}, + }, { id: 'status', header: t('provisioning.recent-jobs.column-status', 'Status'), diff --git a/public/app/features/provisioning/Repository/RepositoryHealth.tsx b/public/app/features/provisioning/Repository/RepositoryHealth.tsx deleted file mode 100644 index daac98dbcaa..00000000000 --- a/public/app/features/provisioning/Repository/RepositoryHealth.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Trans, t } from '@grafana/i18n'; -import { Stack, Alert, Text } from '@grafana/ui'; -import { HealthStatus } from 'app/api/clients/provisioning/v0alpha1'; - -interface Props { - health: HealthStatus; -} - -export function RepositoryHealth({ health }: Props) { - return ( - - {health.healthy ? ( - - No errors found - - ) : ( - - {health.message && health.message.length > 0 && ( - <> - - Details: - -
      - {health.message.map((message) => ( -
    • {message}
    • - ))} -
    - - )} -
    - )} -
    - ); -} diff --git a/public/app/features/provisioning/Repository/RepositoryHealthCard.tsx b/public/app/features/provisioning/Repository/RepositoryHealthCard.tsx new file mode 100644 index 00000000000..4c641c85a04 --- /dev/null +++ b/public/app/features/provisioning/Repository/RepositoryHealthCard.tsx @@ -0,0 +1,88 @@ +import { css } from '@emotion/css'; + +import { t, Trans } from '@grafana/i18n'; +import { Badge, Card, Grid, Stack, Text, useStyles2 } from '@grafana/ui'; +import { Repository } from 'app/api/clients/provisioning/v0alpha1'; + +import { formatTimestamp } from '../utils/time'; + +import { CheckRepository } from './CheckRepository'; + +export function RepositoryHealthCard({ repo }: { repo: Repository }) { + const styles = useStyles2(getStyles); + const status = repo.status; + + return ( + + + Health + + + + {/* Status */} + + Status: + + +
    + +
    + + {/* Checked */} + + Checked: + +
    + {formatTimestamp(status?.health?.checked)} +
    + + {!!status?.health?.message?.length && ( + <> +
    + + Messages: + +
    +
    + + {status.health.message.map((msg, idx) => ( + + {msg} + + ))} + +
    + + )} +
    +
    + + + +
    + ); +} + +const getStyles = () => { + return { + spanTwo: css({ + gridColumn: 'span 2', + }), + card: css({ + height: '100%', + display: 'flex', + flexDirection: 'column', + }), + actions: css({ + marginTop: 'auto', + }), + }; +}; diff --git a/public/app/features/provisioning/Repository/RepositoryOverview.tsx b/public/app/features/provisioning/Repository/RepositoryOverview.tsx index b176cbf712d..252566f4242 100644 --- a/public/app/features/provisioning/Repository/RepositoryOverview.tsx +++ b/public/app/features/provisioning/Repository/RepositoryOverview.tsx @@ -1,25 +1,26 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import { useMemo } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaEdition } from '@grafana/data/internal'; -import { Trans, t } from '@grafana/i18n'; +import { Trans } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { Box, Card, CellProps, Grid, InteractiveTable, LinkButton, Stack, Text, useStyles2 } from '@grafana/ui'; import { Repository, ResourceCount } from 'app/api/clients/provisioning/v0alpha1'; import { RecentJobs } from '../Job/RecentJobs'; -import { MessageList } from '../Shared/MessageList'; import { formatTimestamp } from '../utils/time'; -import { CheckRepository } from './CheckRepository'; -import { RepositoryHealth } from './RepositoryHealth'; -import { SyncRepository } from './SyncRepository'; +import { RepositoryHealthCard } from './RepositoryHealthCard'; +import { RepositoryPullStatusCard } from './RepositoryPullStatusCard'; type StatCell = CellProps; -function getColumnCount(hasWebhook: boolean): 3 | 4 { - return hasWebhook ? 4 : 3; +function getColumnCount(hasWebhook: boolean): { xxlColumn: 5 | 4; lgColumn: 3 | 2 } { + return { + xxlColumn: hasWebhook ? 5 : 4, + lgColumn: hasWebhook ? 3 : 2, + }; } export function RepositoryOverview({ repo }: { repo: Repository }) { @@ -27,7 +28,7 @@ export function RepositoryOverview({ repo }: { repo: Repository }) { const status = repo.status; const webhookURL = getWebhookURL(repo); - const columns = getColumnCount(Boolean(repo.status?.webhook)); + const { lgColumn, xxlColumn } = getColumnCount(Boolean(repo.status?.webhook)); const resourceColumns = useMemo( () => [ @@ -53,7 +54,7 @@ export function RepositoryOverview({ repo }: { repo: Repository }) { return ( - +
    @@ -69,144 +70,20 @@ export function RepositoryOverview({ repo }: { repo: Repository }) { ) : null} - + View Folder
    + {repo.status?.health && (
    - - - Health - - - - -
    - - Status: - -
    -
    - - {status?.health?.healthy - ? t('provisioning.repository-overview.healthy', 'Healthy') - : t('provisioning.repository-overview.unhealthy', 'Unhealthy')} - -
    - -
    - - Checked: - -
    -
    - {formatTimestamp(status?.health?.checked)} -
    - - {!!status?.health?.message?.length && ( - <> -
    - - Messages: - -
    -
    - - {status.health.message.map((msg, idx) => ( - - {msg} - - ))} - -
    - - )} -
    -
    - - - -
    +
    )} -
    - - - Pull status - - - -
    - - Status: - -
    -
    - {status?.sync.state ?? 'N/A'} -
    -
    - - Job ID: - -
    -
    - {status?.sync.job ?? 'N/A'} -
    - -
    - - Last Ref: - -
    -
    - - {status?.sync.lastRef - ? status.sync.lastRef.substring(0, 7) - : t('provisioning.repository-overview.not-available', 'N/A')} - -
    - -
    - - Started: - -
    -
    - {formatTimestamp(status?.sync.started)} -
    - -
    - - Finished: - -
    -
    - {formatTimestamp(status?.sync.finished)} -
    - - {!!status?.sync?.message?.length && ( - <> -
    - - Messages: - -
    -
    - -
    - - )} -
    -
    - - - -
    -
    + {/* Webhook */} {repo.status?.webhook && (
    @@ -251,6 +128,16 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
    )} + + {/* Pull status */} +
    + +
    {/* job status is not ready for Cloud yet */} @@ -292,6 +179,23 @@ const getStyles = (theme: GrafanaTheme2) => { valueColumn: css({ gridColumn: 'span 9', }), + pullStatusCard: css({ + gridColumn: 'span 2', + + [theme.breakpoints.down('lg')]: { + gridColumn: 'span 2', + }, + }), + pullStatusCardLgSpan3: css({ + [theme.breakpoints.down('xxl')]: { + gridColumn: 'span 3', + }, + }), + pullStatusCardLgSpan2: css({ + [theme.breakpoints.down('xxl')]: { + gridColumn: 'span 2', + }, + }), }; }; diff --git a/public/app/features/provisioning/Repository/RepositoryPullStatusCard.tsx b/public/app/features/provisioning/Repository/RepositoryPullStatusCard.tsx new file mode 100644 index 00000000000..8758f8de911 --- /dev/null +++ b/public/app/features/provisioning/Repository/RepositoryPullStatusCard.tsx @@ -0,0 +1,99 @@ +import { css } from '@emotion/css'; + +import { t, Trans } from '@grafana/i18n'; +import { Badge, Card, Grid, Text, TextLink, useStyles2 } from '@grafana/ui'; +import { Repository } from 'app/api/clients/provisioning/v0alpha1'; + +import { MessageList } from '../Shared/MessageList'; +import { getRepoCommitUrl } from '../utils/git'; +import { getStatusColor, getStatusIcon } from '../utils/repositoryStatus'; +import { formatTimestamp } from '../utils/time'; + +import { SyncRepository } from './SyncRepository'; + +export function RepositoryPullStatusCard({ repo }: { repo: Repository }) { + const styles = useStyles2(getStyles); + const status = repo.status; + const statusColor = getStatusColor(status?.sync.state); + const statusIcon = getStatusIcon(status?.sync.state); + + const { url: lastCommitUrl, hasUrl } = getRepoCommitUrl(repo.spec, status?.sync.lastRef); + + return ( + + + Pull status + + + + {/* Status */} + + Status: + +
    + +
    + + {/* Job ID */} + + Job ID: + +
    + {status?.sync.job ?? 'N/A'} +
    + + {/* Last Ref */} + + Last Ref: + +
    + {hasUrl && lastCommitUrl ? ( + + + {status?.sync.lastRef + ? status.sync.lastRef.substring(0, 7) + : t('provisioning.repository-overview.not-available', 'N/A')} + + + ) : ( + + {status?.sync.lastRef + ? status.sync.lastRef.substring(0, 7) + : t('provisioning.repository-overview.not-available', 'N/A')} + + )} +
    + + + Last successful pull: + +
    + {formatTimestamp(status?.sync.finished)} +
    + + {!!status?.sync?.message?.length && ( + <> + + Messages: + +
    + +
    + + )} +
    +
    + + + +
    + ); +} + +const getStyles = () => { + return { + spanTwo: css({ + gridColumn: 'span 2', + }), + }; +}; diff --git a/public/app/features/provisioning/utils/git.ts b/public/app/features/provisioning/utils/git.ts index a5d14b3cb5a..7bd221851a2 100644 --- a/public/app/features/provisioning/utils/git.ts +++ b/public/app/features/provisioning/utils/git.ts @@ -70,3 +70,40 @@ export const getRepoHrefForProvider = (spec?: RepositorySpec) => { export function getHasTokenInstructions(type: RepoType): type is InstructionAvailability { return type === 'github' || type === 'gitlab' || type === 'bitbucket'; } + +export function getRepoCommitUrl(spec?: RepositorySpec, commit?: string) { + let url: string | undefined = undefined; + let hasUrl = false; + + if (!spec || !spec.type || !commit) { + return { hasUrl, url }; + } + + const gitType = spec.type; + + // local repositories don't have a URL + if (gitType !== 'local' && commit) { + switch (gitType) { + case 'github': + if (spec.github?.url) { + url = `${spec.github.url}/commit/${commit}`; + hasUrl = true; + } + break; + case 'gitlab': + if (spec.gitlab?.url) { + url = `${spec.gitlab.url}/-/commit/${commit}`; + hasUrl = true; + } + break; + case 'bitbucket': + if (spec.bitbucket?.url) { + url = `${spec.bitbucket.url}/commits/${commit}`; + hasUrl = true; + } + break; + } + } + + return { hasUrl, url }; +} diff --git a/public/app/features/provisioning/utils/repositoryStatus.ts b/public/app/features/provisioning/utils/repositoryStatus.ts new file mode 100644 index 00000000000..9d899588506 --- /dev/null +++ b/public/app/features/provisioning/utils/repositoryStatus.ts @@ -0,0 +1,42 @@ +import { BadgeColor, IconName } from '@grafana/ui'; +import { SyncStatus } from 'app/api/clients/provisioning/v0alpha1'; + +export interface RepositoryStatus { + color: BadgeColor; + text: string; + icon: IconName; + tooltip?: string; +} + +export const getStatusColor = (state?: SyncStatus['state']) => { + switch (state) { + case 'success': + return 'green'; + case 'working': + return 'blue'; + case 'warning': + return 'orange'; + case 'pending': + return 'darkgrey'; + case 'error': + return 'red'; + default: + return 'darkgrey'; + } +}; + +export const getStatusIcon = (state?: SyncStatus['state']): IconName => { + switch (state) { + case 'success': + return 'check'; + case 'working': + case 'warning': + return 'exclamation-triangle'; + case 'pending': + return 'spinner'; + case 'error': + return 'exclamation-triangle'; + default: + return 'exclamation-triangle'; + } +}; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 5f0f34d651d..629642ee6f9 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -11584,6 +11584,7 @@ "active-jobs": "active jobs", "column-action": "Action", "column-duration": "Duration", + "column-job-id": "Job ID", "column-message": "Message", "column-started": "Started", "column-status": "Status", @@ -11602,12 +11603,6 @@ "settings": "Settings", "view": "View" }, - "repository-health": { - "details": "Details:", - "no-errors-found": "No errors found", - "title-repository-is-healthy": "Repository is healthy", - "title-repository-is-unhealthy": "Repository is unhealthy" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "Compare branch", @@ -11622,7 +11617,7 @@ }, "repository-overview": { "checked": "Checked:", - "finished": "Finished:", + "finished": "Last successful pull:", "health": "Health", "healthy": "Healthy", "job-id": "Job ID:", @@ -11631,7 +11626,6 @@ "not-available": "N/A", "pull-status": "Pull status", "resources": "Resources", - "started": "Started:", "status": "Status:", "unhealthy": "Unhealthy", "view-folder": "View Folder", From d408e712e5d071bd81794fbcdd8946c9c190cb15 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Thu, 9 Oct 2025 16:51:36 +0300 Subject: [PATCH 096/578] Repeats: Add E2E tests for tabs layout repeats (#112109) * add e2e tests for repeated tabs * finalise tab repeat e2e tests --- .github/CODEOWNERS | 1 + .../dashboards-repeats-custom-grid.spec.ts | 1 - .../dashboards-repeats-tabs-layout.spec.ts | 487 ++++++++++ e2e-playwright/dashboard-new-layouts/utils.ts | 45 + .../dashboards/V2DashWithTabRepeats.json | 837 ++++++++++++++++++ .../src/selectors/components.ts | 5 + .../scene/layout-tabs/TabItemEditor.tsx | 1 + .../scene/layout-tabs/TabItemRenderer.tsx | 2 +- 8 files changed, 1377 insertions(+), 2 deletions(-) create mode 100644 e2e-playwright/dashboard-new-layouts/dashboards-repeats-tabs-layout.spec.ts create mode 100644 e2e-playwright/dashboards/V2DashWithTabRepeats.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2af83368859..114a4d8feed 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -429,6 +429,7 @@ /e2e-playwright/dashboards/TestDashboard.json @grafana/dashboards-squad @grafana/grafana-search-navigate-organise /e2e-playwright/dashboards/TestV2Dashboard.json @grafana/dashboards-squad /e2e-playwright/dashboards/V2DashWithRepeats.json @grafana/dashboards-squad +/e2e-playwright/dashboards/V2DashWithTabRepeats.json @grafana/dashboards-squad /e2e-playwright/dashboards-suite/adhoc-filter-from-panel.spec.ts @grafana/datapro /e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts @grafana/grafana-search-navigate-organise /e2e-playwright/dashboards-suite/dashboard-browse.spec.ts @grafana/grafana-search-navigate-organise diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts index 0a349df5728..2d235571f97 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts @@ -1,6 +1,5 @@ import { test, expect } from '@grafana/plugin-e2e'; -import testV2Dashboard from '../dashboards/TestV2Dashboard.json'; import testV2DashWithRepeats from '../dashboards/V2DashWithRepeats.json'; import { diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-tabs-layout.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-tabs-layout.spec.ts new file mode 100644 index 00000000000..100ec864b0d --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-tabs-layout.spec.ts @@ -0,0 +1,487 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import V2DashWithTabRepeats from '../dashboards/V2DashWithTabRepeats.json'; + +import { + verifyChanges, + saveDashboard, + importTestDashboard, + goToEmbeddedPanel, + checkRepeatedTabTitles, + groupIntoTab, + moveTab, + getTabPosition, +} from './utils'; + +const repeatTitleBase = 'Tab - '; +const newTitleBase = 'edited tab rep - '; +const repeatOptions = [1, 2, 3, 4]; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +test.use({ + viewport: { width: 1920, height: 1080 }, +}); + +test.describe( + 'Repeats - Dashboard tabs layout', + { + tag: ['@dashboards'], + }, + () => { + test('can enable tab repeats', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Tabs layout repeats - add repeats'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoTab(page, dashboardPage, selectors); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput) + .fill(`${repeatTitleBase}$c1`); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.join(' + ')}`) + ) + ).toBeVisible(); + + const repeatOptionsGroup = dashboardPage.getByGrafanaSelector( + selectors.components.OptionsGroup.group('repeat-options') + ); + // expand repeat options dropdown + await repeatOptionsGroup.getByRole('button').first().click(); + // find repeat variable dropdown + await repeatOptionsGroup.getByRole('combobox').click(); + await page.getByRole('option', { name: 'c1' }).click(); + + await checkRepeatedTabTitles(dashboardPage, selectors, repeatTitleBase, repeatOptions); + + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + await checkRepeatedTabTitles(dashboardPage, selectors, repeatTitleBase, repeatOptions); + }); + + test('can update tab repeats with variable change', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - update on variable change', + JSON.stringify(V2DashWithTabRepeats) + ); + + const c1Var = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('c1')); + await c1Var + .locator('..') + .getByTestId(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(repeatOptions.join(','))) + .click(); + // deselect last variable option + await dashboardPage + .getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts(`${repeatOptions.at(-1)}`) + ) + .click(); + await page.locator('body').click({ position: { x: 0, y: 0 } }); // blur select + + // verify that repeats are present for first 3 values + await checkRepeatedTabTitles(dashboardPage, selectors, repeatTitleBase, repeatOptions.slice(0, -1)); + // verify there is no repeat with last value + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(-1)}`)) + ).toBeHidden(); + }); + test('can update repeats in edit pane', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - update through edit pane', + JSON.stringify(V2DashWithTabRepeats) + ); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + // select first/original repeat tab to activate edit pane + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(0)}`)) + .click(); + + const titleInput = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput + ); + await titleInput.fill(`${newTitleBase}$c1`); + await titleInput.blur(); + + await checkRepeatedTabTitles(dashboardPage, selectors, newTitleBase, repeatOptions); + + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + await checkRepeatedTabTitles(dashboardPage, selectors, newTitleBase, repeatOptions); + }); + + test('can update repeats after panel change', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - update repeats after panel change', + JSON.stringify(V2DashWithTabRepeats) + ); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first().click(); + + const panelTitleInput = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldInput('Title') + ); + await panelTitleInput.fill('New edited panel'); + await panelTitleInput.blur(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(1)}`)) + .click(); + + // intermediate step to verify tab switch happened + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 2 - Row 1 - Panel repeat 1')) + ).toBeVisible(); + + // verify edited panel title updated in repeated tab + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New edited panel')) + ).toBeVisible(); + + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New edited panel')) + ).toBeVisible(); + }); + + test('can update repeats after panel change in editor', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - update repeats after panel change in editor', + JSON.stringify(V2DashWithTabRepeats) + ); + + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first(); + await panel.hover(); + await page.keyboard.press('e'); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardEditPaneSplitter.primaryBody) + ).toBeHidden(); // verifying that panel editor loaded + + const panelTitleInput = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldInput('Title') + ); + await panelTitleInput.fill('New edited panel'); + await panelTitleInput.blur(); + + // playwright too fast, verifying JSON diff that changes landed + await verifyChanges(dashboardPage, page, selectors, 'New edited panel'); + + // verify panel title change in panel editor UI + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(`New edited panel`)) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardEditPaneSplitter.primaryBody) + ).toBeVisible(); // verifying that dashboard loaded + + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(1)}`)) + .click(); + + // intermediate step to verify tab switch happened + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 2 - Row 1 - Panel repeat 1')) + ).toBeVisible(); + + // verify edited panel title updated in repeated tab + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New edited panel')) + ).toBeVisible(); + + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + // verify edited panel title updated in repeated tab + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New edited panel')) + ).toBeVisible(); + }); + + test('can hide canvas grid add row action in repeats', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - hide canvas add action in repeats', + JSON.stringify(V2DashWithTabRepeats) + ); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow)).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(1)}`)) + .click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow)).toBeHidden(); + }); + + test('can move repeated tabs', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - move repeated tabs', + JSON.stringify(V2DashWithTabRepeats) + ); + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await moveTab(dashboardPage, page, selectors, `${repeatTitleBase}${repeatOptions.at(0)}`, 'New tab'); + + // playwright too fast - adding intermediate step so that UI has time to update + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab')).click(); + + // verify move by tab position + const repeatedTab = await getTabPosition(dashboardPage, selectors, `${repeatTitleBase}${repeatOptions.at(0)}`); + const normalTab = await getTabPosition(dashboardPage, selectors, 'New tab'); + expect(normalTab?.x).toBeLessThan(repeatedTab?.x || 0); + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + const repeatedTab2 = await getTabPosition(dashboardPage, selectors, `${repeatTitleBase}${repeatOptions.at(0)}`); + const normalTab2 = await getTabPosition(dashboardPage, selectors, 'New tab'); + expect(normalTab2?.x).toBeLessThan(repeatedTab2?.x || 0); + }); + + test('can load into repeated tab', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - can load into repeated tab', + JSON.stringify(V2DashWithTabRepeats) + ); + + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(2)}`)) + .click(); + + await page.reload(); + + await expect(page.locator('[data-testid="uplot-main-div"]').first()).toBeVisible(); + + expect( + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(2)}`)) + .getAttribute('aria-selected') + ).toBe('true'); + }); + + test('can view panels in repeated tab', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - view panels in repeated tabs', + JSON.stringify(V2DashWithTabRepeats) + ); + + // non repeated panel in repeated tab + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first().hover(); + await page.keyboard.press('v'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 1 - Panel repeat 1')) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toBeVisible(); + + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + + // repeated panel in original tab repeat + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('Row 2')) + .scrollIntoViewIfNeeded(); + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 2 - Panel repeat 2')) + .hover(); + await page.keyboard.press('v'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 1 - Panel repeat 1')) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 2 - Panel repeat 2')) + ).toBeVisible(); + + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 2 - Panel repeat 2')) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + + // repeated panel in repeated tab + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(2)}`)) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('Row 2')) + .scrollIntoViewIfNeeded(); + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 3 - Row 2 - Panel repeat 2')) + .hover(); + await page.keyboard.press('v'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 3 - Row 1 - Panel repeat 1')) + ).toBeHidden(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 3 - Row 2 - Panel repeat 2')) + ).toBeVisible(); + + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 3 - Row 2 - Panel repeat 2')) + ).toBeVisible(); + }); + + test('can view embedded panels in repeated tab', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - view embedded panels in repeated tabs', + JSON.stringify(V2DashWithTabRepeats) + ); + + const dashUrl = page.url(); + + // non repeated panel in repeated tab + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first().hover(); + await page.keyboard.press('p+e'); + await goToEmbeddedPanel(page); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toBeVisible(); + await page.goto(dashUrl); + + // repeated panel in original tab repeat + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('Row 2')) + .scrollIntoViewIfNeeded(); + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 2 - Panel repeat 2')) + .hover(); + await page.keyboard.press('p+e'); + await goToEmbeddedPanel(page); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 2 - Panel repeat 2')) + ).toBeVisible(); + await page.goto(dashUrl); + + // repeated panel in repeated tab + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(2)}`)) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('Row 2')) + .scrollIntoViewIfNeeded(); + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 3 - Row 2 - Panel repeat 2')) + .hover(); + await page.keyboard.press('p+e'); + await goToEmbeddedPanel(page); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 3 - Row 2 - Panel repeat 2')) + ).toBeVisible(); + }); + + test('can remove repeats', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - remove repeats', + JSON.stringify(V2DashWithTabRepeats) + ); + + // verify 5 tabs are present (4 repeats and 1 normal) + await checkRepeatedTabTitles(dashboardPage, selectors, repeatTitleBase, repeatOptions); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(0)}`)) + .click(); + + const repeatOptionsGroup = dashboardPage.getByGrafanaSelector( + selectors.components.OptionsGroup.group('repeat-options') + ); + // expand repeat options dropdown + await repeatOptionsGroup.getByRole('button').first().click(); + // find repeat variable dropdown + await repeatOptionsGroup.getByRole('combobox').click(); + await page.getByRole('option', { name: 'Disable repeating' }).click(); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.join(' + ')}`) + ) + ).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(1)}`)) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(2)}`)) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(3)}`)) + ).toBeHidden(); + + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.join(' + ')}`) + ) + ).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(1)}`)) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(2)}`)) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(3)}`)) + ).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/utils.ts b/e2e-playwright/dashboard-new-layouts/utils.ts index 2d279b0545a..70f5aa41bf0 100644 --- a/e2e-playwright/dashboard-new-layouts/utils.ts +++ b/e2e-playwright/dashboard-new-layouts/utils.ts @@ -187,3 +187,48 @@ export async function goToEmbeddedPanel(page: Page) { page.goto(soloPanelUrl!); } + +export async function moveTab( + dashboardPage: DashboardPage, + page: Page, + selectors: E2ESelectorGroups, + sourceTab: string, + targetTab: string +) { + // Get target panel position + const targetTabElement = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(targetTab)).first(); + + // Get source panel element + const sourceTabElement = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(sourceTab)).first(); + + const targetBox = await targetTabElement.boundingBox(); + + // Perform drag and drop (dragTo() did not work in this case) + await sourceTabElement.hover(); + await page.mouse.down(); + // move to adjusted target position (relative to top left) + await page.mouse.move((targetBox?.x || 0) + (targetBox?.width || 0), targetBox?.y || 0, { steps: 5 }); + await page.mouse.up(); +} + +export async function groupIntoTab(page: Page, dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).click(); + await page.getByText('Group into tab').click(); +} + +export async function checkRepeatedTabTitles( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + title: string, + options: Array +) { + for (const option of options) { + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`${title}${option}`))).toBeVisible(); + } +} + +export async function getTabPosition(dashboardPage: DashboardPage, selectors: E2ESelectorGroups, tabTitle: string) { + const tab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(tabTitle)).first(); + const boundingBox = await tab.boundingBox(); + return boundingBox; +} diff --git a/e2e-playwright/dashboards/V2DashWithTabRepeats.json b/e2e-playwright/dashboards/V2DashWithTabRepeats.json new file mode 100644 index 00000000000..99cabae8d94 --- /dev/null +++ b/e2e-playwright/dashboards/V2DashWithTabRepeats.json @@ -0,0 +1,837 @@ +{ + "apiVersion": "dashboard.grafana.app/v2beta1", + "kind": "Dashboard", + "metadata": { + "name": "addwm76", + "namespace": "default", + "uid": "TOro1wyxnlZJEWBZN3MF2MuXtdBb1GOnG6zynaCifi4X", + "resourceVersion": "3", + "generation": 4, + "creationTimestamp": "2025-09-18T08:03:45Z", + "labels": {}, + "annotations": {} + }, + "spec": { + "annotations": [], + "cursorSync": "Off", + "description": "", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "PD8C576611E62080A" + }, + "group": "grafana-testdata-datasource", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 1, + "links": [], + "title": "Tab $c1 - Row $c2 - Panel repeat $c3", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "12.3.0-pre" + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "PD8C576611E62080A" + }, + "group": "grafana-testdata-datasource", + "kind": "DataQuery", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 1 + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 2, + "links": [], + "title": "New panel", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "12.3.0-pre" + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "PD8C576611E62080A" + }, + "group": "grafana-testdata-datasource", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 3, + "links": [], + "title": "New panel", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "12.3.0-pre" + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "PD8C576611E62080A" + }, + "group": "grafana-testdata-datasource", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 4, + "links": [], + "title": "New panel", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "12.3.0-pre" + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "PD8C576611E62080A" + }, + "group": "grafana-testdata-datasource", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 5, + "links": [], + "title": "New panel", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "12.3.0-pre" + } + } + } + }, + "layout": { + "kind": "TabsLayout", + "spec": { + "tabs": [ + { + "kind": "TabsLayoutTab", + "spec": { + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "layout": { + "kind": "AutoGridLayout", + "spec": { + "columnWidthMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + } + ], + "maxColumnCount": 3, + "rowHeightMode": "standard" + } + }, + "title": "New row" + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + }, + "height": 8, + "repeat": { + "direction": "h", + "mode": "variable", + "value": "c3" + }, + "width": 24, + "x": 0, + "y": 0 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + }, + "height": 8, + "width": 12, + "x": 0, + "y": 8 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + }, + "height": 8, + "width": 12, + "x": 12, + "y": 8 + } + } + ] + } + }, + "repeat": { + "mode": "variable", + "value": "c2" + }, + "title": "Row $c2" + } + } + ] + } + }, + "repeat": { + "mode": "variable", + "value": "c1" + }, + "title": "Tab - $c1" + } + }, + { + "kind": "TabsLayoutTab", + "spec": { + "layout": { + "kind": "AutoGridLayout", + "spec": { + "columnWidthMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ], + "maxColumnCount": 3, + "rowHeightMode": "standard" + } + }, + "title": "New tab" + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "autoRefresh": "", + "autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "fiscalYearStartMonth": 0, + "from": "now-6h", + "hideTimepicker": false, + "timezone": "browser", + "to": "now" + }, + "title": "tab repeats", + "variables": [ + { + "kind": "CustomVariable", + "spec": { + "allowCustomValue": true, + "current": { + "text": ["1", "2", "3", "4"], + "value": ["1", "2", "3", "4"] + }, + "hide": "dontHide", + "includeAll": true, + "multi": true, + "name": "c1", + "options": [ + { + "selected": true, + "text": "1", + "value": "1" + }, + { + "selected": true, + "text": "2", + "value": "2" + }, + { + "selected": true, + "text": "3", + "value": "3" + }, + { + "selected": true, + "text": "4", + "value": "4" + } + ], + "query": "1,2,3,4", + "skipUrlSync": false + } + }, + { + "kind": "CustomVariable", + "spec": { + "allowCustomValue": true, + "current": { + "text": ["1", "2", "3", "4"], + "value": ["1", "2", "3", "4"] + }, + "hide": "dontHide", + "includeAll": true, + "multi": true, + "name": "c2", + "options": [ + { + "selected": true, + "text": "1", + "value": "1" + }, + { + "selected": true, + "text": "2", + "value": "2" + }, + { + "selected": true, + "text": "3", + "value": "3" + }, + { + "selected": true, + "text": "4", + "value": "4" + } + ], + "query": "1,2,3,4", + "skipUrlSync": false + } + }, + { + "kind": "CustomVariable", + "spec": { + "allowCustomValue": true, + "current": { + "text": ["1", "2", "3", "4"], + "value": ["1", "2", "3", "4"] + }, + "hide": "dontHide", + "includeAll": false, + "multi": true, + "name": "c3", + "options": [ + { + "selected": true, + "text": "1", + "value": "1" + }, + { + "selected": true, + "text": "2", + "value": "2" + }, + { + "selected": true, + "text": "3", + "value": "3" + }, + { + "selected": true, + "text": "4", + "value": "4" + } + ], + "query": "1,2,3,4", + "skipUrlSync": false + } + } + ] + }, + "status": {} +} diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 05b0f553578..9dc1afc8a89 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -670,6 +670,11 @@ export const versionedComponents = { '12.1.0': 'data-testid fill screen switch', }, }, + TabsLayout: { + titleInput: { + '12.2.0': 'data-testid tab title input', + }, + }, }, }, PanelInspector: { diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx index ede733f4f42..3c4466e8866 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx @@ -91,6 +91,7 @@ function TabTitleInput({ tab, isNewElement, id }: { tab: TabItem; isNewElement: onFocus={() => (prevTitle.current = title || '')} onBlur={() => editTabTitleAction(tab, title || '', prevTitle.current || '')} onChange={(e) => tab.onChangeTitle(e.currentTarget.value)} + data-testid={selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput} /> ); diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx index 06d526420a1..65621b516c4 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx @@ -24,7 +24,7 @@ export function TabItemRenderer({ model }: SceneComponentProps) { const mySlug = model.getSlug(); const urlKey = parentLayout.getUrlKey(); const isActive = mySlug === currentTabSlug; - const myIndex = parentLayout.state.tabs.findIndex((tab) => tab === model); + const myIndex = parentLayout.getTabsIncludingRepeats().findIndex((tab) => tab === model); const location = useLocation(); const href = textUtil.sanitize(locationUtil.getUrlForPartial(location, { [urlKey]: mySlug })); const styles = useStyles2(getStyles); From bfcd8b8f48171b12cabb6d9750983688f2d9d578 Mon Sep 17 00:00:00 2001 From: Jesse David Peterson Date: Thu, 9 Oct 2025 10:26:06 -0400 Subject: [PATCH 097/578] fix(lint): make eslint ignore the coverage directory (#112218) --- eslint.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/eslint.config.js b/eslint.config.js index a0245a55b48..cb8fba5222d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -100,6 +100,7 @@ module.exports = [ '**/build/', '**/compiled/', '**/dist/', + 'coverage/', 'data/', 'deployment_tools_config.json', 'devenv', From d61abe95ad042dfb58a33247425e191677513f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Thu, 9 Oct 2025 16:42:02 +0200 Subject: [PATCH 098/578] unified-storage: Rebuild indexes with recently-imported resources (#112202) * Use timestamps reported via GetResourceLastImportTimes to trigger index rebuilds. * Add test for old last import time. * Don't reindex after bulk-import. It is now done indirectly via LastImportTime on all instances that own the index. --- pkg/storage/unified/resource/bulk.go | 18 -------- pkg/storage/unified/resource/search.go | 49 ++++++++++++++++++--- pkg/storage/unified/resource/search_test.go | 47 ++++++++++++++++---- 3 files changed, 80 insertions(+), 34 deletions(-) diff --git a/pkg/storage/unified/resource/bulk.go b/pkg/storage/unified/resource/bulk.go index 913477fa3e3..935204384df 100644 --- a/pkg/storage/unified/resource/bulk.go +++ b/pkg/storage/unified/resource/bulk.go @@ -250,24 +250,6 @@ func (s *server) BulkProcess(stream resourcepb.BulkStore_BulkProcessServer) erro rsp.Error = AsErrorResult(runner.err) } - if rsp.Error == nil && s.search != nil { - // Rebuild any changed indexes - for _, summary := range rsp.Summary { - _, err := s.search.build(ctx, NamespacedResource{ - Namespace: summary.Namespace, - Group: summary.Group, - Resource: summary.Resource, - }, summary.Count, "rebuildAfterBatchLoad", true) - if err != nil { - s.log.Warn("error building search index after batch load", "err", err) - rsp.Error = &resourcepb.ErrorResult{ - Code: http.StatusInternalServerError, - Message: "err building search index: " + summary.Resource, - Reason: err.Error(), - } - } - } - } return sendAndClose(rsp) } diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index 50541be922f..edaaa0abba6 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -236,6 +236,11 @@ func combineRebuildRequests(a, b rebuildRequest) (c rebuildRequest, ok bool) { ret.minBuildTime = b.minBuildTime } + // Using higher "last import time" is stricter condition, and causes more indexes to be rebuilt. + if a.lastImportTime.IsZero() || (!b.lastImportTime.IsZero() && b.lastImportTime.After(a.lastImportTime)) { + ret.lastImportTime = b.lastImportTime + } + return ret, true } @@ -540,12 +545,16 @@ func (s *searchSupport) runPeriodicScanForIndexesToRebuild(ctx context.Context) s.log.Info("stopping periodic index rebuild due to context cancellation") return case <-ticker.C: - s.findIndexesToRebuild(time.Now()) + importTimes, err := s.getLastImportTimes(ctx) + if err != nil { + s.log.Error("failed to get import times", "error", err) + } + s.findIndexesToRebuild(importTimes, time.Now()) } } } -func (s *searchSupport) findIndexesToRebuild(now time.Time) { +func (s *searchSupport) findIndexesToRebuild(lastImportTimes map[NamespacedResource]time.Time, now time.Time) { // Check all open indexes and see if any of them need to be rebuilt. // This is done periodically to make sure that the indexes are up to date. @@ -567,17 +576,20 @@ func (s *searchSupport) findIndexesToRebuild(now time.Time) { minBuildTime = now.Add(-maxAge) } + lastImportTime := lastImportTimes[key] // Will be time.Time{} if not found. + bi, err := idx.BuildInfo() if err != nil { s.log.Error("failed to get build info for index to rebuild", "key", key, "error", err) continue } - if shouldRebuildIndex(s.minBuildVersion, bi, minBuildTime, nil) { + if shouldRebuildIndex(bi, s.minBuildVersion, minBuildTime, lastImportTime, nil) { s.rebuildQueue.Add(rebuildRequest{ NamespacedResource: key, minBuildTime: minBuildTime, minBuildVersion: s.minBuildVersion, + lastImportTime: lastImportTime, }) if s.indexMetrics != nil { @@ -587,6 +599,18 @@ func (s *searchSupport) findIndexesToRebuild(now time.Time) { } } +func (s *searchSupport) getLastImportTimes(ctx context.Context) (map[NamespacedResource]time.Time, error) { + result := map[NamespacedResource]time.Time{} + for importTime, err := range s.storage.GetResourceLastImportTimes(ctx) { + if err != nil { + // We return times that we have collected so far, if any. + return result, err + } + result[importTime.NamespacedResource] = importTime.LastImportTime + } + return result, nil +} + // runIndexRebuilder is a goroutine waiting for rebuild requests, and rebuilds indexes specified in those requests. // Rebuild requests can be generated periodically (if configured), or after new documents have been imported into the storage with old RVs. func (s *searchSupport) runIndexRebuilder(ctx context.Context) { @@ -626,7 +650,7 @@ func (s *searchSupport) rebuildIndex(ctx context.Context, req rebuildRequest) { l.Error("failed to get build info for index to rebuild", "error", err) } - rebuild := shouldRebuildIndex(req.minBuildVersion, bi, req.minBuildTime, l) + rebuild := shouldRebuildIndex(bi, req.minBuildVersion, req.minBuildTime, req.lastImportTime, l) if !rebuild { span.AddEvent("index not rebuilt") l.Info("index doesn't need to be rebuilt") @@ -662,7 +686,7 @@ func (s *searchSupport) rebuildIndex(ctx context.Context, req rebuildRequest) { } } -func shouldRebuildIndex(minBuildVersion *semver.Version, buildInfo IndexBuildInfo, minBuildTime time.Time, rebuildLogger *slog.Logger) bool { +func shouldRebuildIndex(buildInfo IndexBuildInfo, minBuildVersion *semver.Version, minBuildTime time.Time, lastImportTime time.Time, rebuildLogger *slog.Logger) bool { if !minBuildTime.IsZero() { if buildInfo.BuildTime.IsZero() || buildInfo.BuildTime.Before(minBuildTime) { if rebuildLogger != nil { @@ -672,6 +696,16 @@ func shouldRebuildIndex(minBuildVersion *semver.Version, buildInfo IndexBuildInf } } + // This is technically the same as minBuildTime, but we want to log a different message to make the rebuild reason clear. + if !lastImportTime.IsZero() { + if buildInfo.BuildTime.IsZero() || buildInfo.BuildTime.Before(lastImportTime) { + if rebuildLogger != nil { + rebuildLogger.Info("index build time is before lastImportTime, rebuilding the index", "indexBuildTime", buildInfo.BuildTime, "lastImportTime", lastImportTime) + } + return true + } + } + if minBuildVersion != nil { if buildInfo.BuildVersion == nil || buildInfo.BuildVersion.Compare(minBuildVersion) < 0 { if rebuildLogger != nil { @@ -687,8 +721,9 @@ func shouldRebuildIndex(minBuildVersion *semver.Version, buildInfo IndexBuildInf type rebuildRequest struct { NamespacedResource - minBuildTime time.Time // if not zero, only rebuild index if it has been built before this timestamp - minBuildVersion *semver.Version // if not nil, only rebuild index with build version older than this. + minBuildTime time.Time // if not zero, rebuild index if it has been built before this timestamp + lastImportTime time.Time // if not zero, rebuild index if it has been built before this timestamp. + minBuildVersion *semver.Version // if not nil, rebuild index with build version older than this. } func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedResource, reason string) (ResourceIndex, error) { diff --git a/pkg/storage/unified/resource/search_test.go b/pkg/storage/unified/resource/search_test.go index 15c6d9770f5..a35a8be7052 100644 --- a/pkg/storage/unified/resource/search_test.go +++ b/pkg/storage/unified/resource/search_test.go @@ -9,8 +9,6 @@ import ( "testing" "time" - "log/slog" - "github.com/Masterminds/semver" "github.com/grafana/authlib/types" "github.com/stretchr/testify/mock" @@ -18,7 +16,6 @@ import ( "go.opentelemetry.io/otel/trace/noop" dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" - "github.com/grafana/grafana/pkg/infra/log/logtest" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) @@ -442,6 +439,7 @@ func TestShouldRebuildIndex(t *testing.T) { type testcase struct { buildInfo IndexBuildInfo minTime time.Time + lastImportTime time.Time minBuildVersion *semver.Version expected bool @@ -459,6 +457,11 @@ func TestShouldRebuildIndex(t *testing.T) { minTime: now, expected: true, }, + "empty build info, with lastImportTime": { + buildInfo: IndexBuildInfo{}, + lastImportTime: now, + expected: true, + }, "empty build info, with minVersion": { buildInfo: IndexBuildInfo{}, minBuildVersion: semver.MustParse("10.15.20"), @@ -474,6 +477,16 @@ func TestShouldRebuildIndex(t *testing.T) { minTime: now, expected: false, }, + "build time before last import time": { + buildInfo: IndexBuildInfo{BuildTime: now.Add(-2 * time.Hour)}, + lastImportTime: now, + expected: true, + }, + "build time after last import time": { + buildInfo: IndexBuildInfo{BuildTime: now.Add(2 * time.Hour)}, + lastImportTime: now, + expected: false, + }, "build version before min version": { buildInfo: IndexBuildInfo{BuildVersion: semver.MustParse("10.15.19")}, minBuildVersion: semver.MustParse("10.15.20"), @@ -486,7 +499,7 @@ func TestShouldRebuildIndex(t *testing.T) { }, } { t.Run(name, func(t *testing.T) { - res := shouldRebuildIndex(tc.minBuildVersion, tc.buildInfo, tc.minTime, slog.New(&logtest.NopHandler{})) + res := shouldRebuildIndex(tc.buildInfo, tc.minBuildVersion, tc.minTime, tc.lastImportTime, nil) require.Equal(t, tc.expected, res) }) } @@ -499,7 +512,7 @@ func TestFindIndexesForRebuild(t *testing.T) { }, } - now := time.Now() + now := time.Now().UTC() search := &mockSearchBackend{ openIndexes: []NamespacedResource{ @@ -511,6 +524,7 @@ func TestFindIndexesForRebuild(t *testing.T) { {Namespace: "resource-v6", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}, {Namespace: "resource-2h-v5", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}, {Namespace: "resource-2h-v6", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}, + {Namespace: "resource-recently-imported", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}, // We report this index as open, but it's really not. This can happen if index expires between the call // to GetOpenIndexes and the call to GetIndex. @@ -557,6 +571,11 @@ func TestFindIndexesForRebuild(t *testing.T) { {Namespace: "resource-2h-v6", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}: &MockResourceIndex{ buildInfo: IndexBuildInfo{BuildTime: now.Add(-2 * time.Hour), BuildVersion: semver.MustParse("6.0.0")}, }, + + // Built recently, to be rebuilt because of last import time + {Namespace: "resource-recently-imported", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}: &MockResourceIndex{ + buildInfo: IndexBuildInfo{BuildTime: now.Add(-30 * time.Minute), BuildVersion: semver.MustParse("6.0.0")}, + }, }, } @@ -579,15 +598,23 @@ func TestFindIndexesForRebuild(t *testing.T) { require.NoError(t, err) require.NotNil(t, support) - support.findIndexesToRebuild(now) - require.Equal(t, 6, support.rebuildQueue.Len()) + lastImportTime := now.Add(-10 * time.Minute) + importTimes := map[NamespacedResource]time.Time{ + {Namespace: "resource-recently-imported", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}: lastImportTime, + + // This index was "just" built, and should not be rebuilt. + {Namespace: "resource-v6", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}: lastImportTime, + } + + support.findIndexesToRebuild(importTimes, now) + require.Equal(t, 7, support.rebuildQueue.Len()) now5m := now.Add(5 * time.Minute) // Running findIndexesToRebuild again should not add any new indexes to the rebuild queue, and all existing // ones should be "combined" with new ones (this will "bump" minBuildTime) - support.findIndexesToRebuild(now5m) - require.Equal(t, 6, support.rebuildQueue.Len()) + support.findIndexesToRebuild(importTimes, now5m) + require.Equal(t, 7, support.rebuildQueue.Len()) // Values that we expect to find in rebuild requests. minBuildVersion := semver.MustParse("5.5.5") @@ -603,6 +630,8 @@ func TestFindIndexesForRebuild(t *testing.T) { {NamespacedResource: NamespacedResource{Namespace: "resource-v5", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTimeDashboard}, {NamespacedResource: NamespacedResource{Namespace: "resource-2h-v5", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTimeDashboard}, {NamespacedResource: NamespacedResource{Namespace: "resource-2h-v6", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTimeDashboard}, + + {NamespacedResource: NamespacedResource{Namespace: "resource-recently-imported", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTimeDashboard, lastImportTime: lastImportTime}, }) } From 0d9b5fafc8589dd3b7964181f9f0ca4733747648 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Thu, 9 Oct 2025 17:24:26 +0200 Subject: [PATCH 099/578] Fix code editor width in dashboard export dialog (#112203) --- .../dashboard-scene/sharing/ExportButton/ExportAsCode.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ExportAsCode.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ExportAsCode.tsx index aabd9707377..9ffad2ef066 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ExportAsCode.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ExportAsCode.tsx @@ -83,8 +83,8 @@ function ExportAsCodeRenderer({ model }: SceneComponentProps) { )}
    - - {({ width, height }) => { + + {({ height }) => { if (stringifiedDashboard) { return ( ) { showLineNumbers={true} showMiniMap={false} height={height} - width={width} + width="100%" readOnly={true} /> ); From b296ea27b80d131d13fe3bbef6b0196a0710878e Mon Sep 17 00:00:00 2001 From: Dhruv Jain Date: Thu, 9 Oct 2025 21:25:04 +0530 Subject: [PATCH 100/578] Dashboard: Add loading state in Permissions tab (#111962) --- .../components/AccessControl/Permissions.tsx | 44 ++++++++++--------- .../ServiceAccountPermissions.tsx | 2 - public/app/features/teams/TeamPermissions.tsx | 1 - public/locales/en-US/grafana.json | 5 +-- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/public/app/core/components/AccessControl/Permissions.tsx b/public/app/core/components/AccessControl/Permissions.tsx index 1b3b3c1e0e3..2ffa29a9f1c 100644 --- a/public/app/core/components/AccessControl/Permissions.tsx +++ b/public/app/core/components/AccessControl/Permissions.tsx @@ -1,11 +1,12 @@ import { css } from '@emotion/css'; import { sortBy } from 'lodash'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import * as React from 'react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { Text, Box, Button, useStyles2 } from '@grafana/ui'; +import { Text, Box, Button, useStyles2, LoadingPlaceholder } from '@grafana/ui'; import { SlideDown } from 'app/core/components/Animations/SlideDown'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { DescendantCount } from 'app/features/browse-dashboards/components/BrowseActions/DescendantCount'; @@ -30,7 +31,6 @@ type ResourceId = string | number; type Type = 'users' | 'teams' | 'serviceAccounts' | 'builtInRoles'; export type Props = { - title?: string; buttonLabel?: string; emptyLabel?: string; addPermissionTitle?: string; @@ -42,7 +42,6 @@ export type Props = { }; export const Permissions = ({ - title = t('access-control.permissions.title', 'Permissions'), buttonLabel = t('access-control.permissions.add-label', 'Add a permission'), emptyLabel = t('access-control.permissions.no-permissions', 'There are no permissions'), resource, @@ -54,23 +53,22 @@ export const Permissions = ({ }: Props) => { const styles = useStyles2(getStyles); const [isAdding, setIsAdding] = useState(false); - const [items, setItems] = useState([]); const [desc, setDesc] = useState(INITIAL_DESCRIPTION); - const fetchItems = useCallback(async () => { + const [permissions, fetchPermissions] = useAsyncFn(async () => { let items = await getPermissions(resource, resourceId); if (getWarnings) { items = getWarnings(items); } - setItems(items); + return items; }, [resource, resourceId, getWarnings]); useEffect(() => { getDescription(resource).then((r) => { setDesc(r); - return fetchItems(); + return fetchPermissions(); }); - }, [resource, resourceId, fetchItems]); + }, [resource, fetchPermissions]); const onAdd = (state: SetPermission) => { let promise: Promise | null = null; @@ -83,7 +81,7 @@ export const Permissions = ({ } if (promise !== null) { - promise.then(fetchItems); + promise.then(fetchPermissions); } }; @@ -98,7 +96,7 @@ export const Permissions = ({ } if (promise !== null) { - promise.then(fetchItems); + promise.then(fetchPermissions); } }; @@ -119,34 +117,34 @@ export const Permissions = ({ const teams = useMemo( () => sortBy( - items.filter((i) => i.teamId), + (permissions.value || []).filter((i) => i.teamId), ['team', 'isManaged'] ), - [items] + [permissions.value] ); const users = useMemo( () => sortBy( - items.filter((i) => i.userId && !i.isServiceAccount), + (permissions.value || []).filter((i) => i.userId && !i.isServiceAccount), ['userLogin', 'isManaged'] ), - [items] + [permissions.value] ); const serviceAccounts = useMemo( () => sortBy( - items.filter((i) => i.userId && i.isServiceAccount), + (permissions.value || []).filter((i) => i.userId && i.isServiceAccount), ['userLogin', 'isManaged'] ), - [items] + [permissions.value] ); const builtInRoles = useMemo( () => sortBy( - items.filter((i) => i.builtInRole), + (permissions.value || []).filter((i) => i.builtInRole), ['builtInRole', 'isManaged'] ), - [items] + [permissions.value] ); const titleRole = t('access-control.permissions.role', 'Role'); @@ -154,6 +152,10 @@ export const Permissions = ({ const titleServiceAccount = t('access-control.permissions.serviceaccount', 'Service Account'); const titleTeam = t('access-control.permissions.team', 'Team'); + if (permissions.loading) { + return ; + } + return ( <>
    @@ -172,7 +174,7 @@ export const Permissions = ({ /> )} - {items.length === 0 && ( + {permissions.value?.length === 0 && ( {emptyLabel} @@ -236,7 +238,7 @@ export const Permissions = ({ )}
    - {epilogue && epilogue(items)} + {epilogue && epilogue(permissions.value || [])} ); }; diff --git a/public/app/features/serviceaccounts/ServiceAccountPermissions.tsx b/public/app/features/serviceaccounts/ServiceAccountPermissions.tsx index 303dfc79716..79fc21cf82a 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPermissions.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPermissions.tsx @@ -1,4 +1,3 @@ -import { t } from '@grafana/i18n'; import { Permissions } from 'app/core/components/AccessControl'; import { contextSrv } from 'app/core/services/context_srv'; import { AccessControlAction } from 'app/types/accessControl'; @@ -16,7 +15,6 @@ export const ServiceAccountPermissions = (props: ServiceAccountPermissionsProps) return ( { return ( Date: Thu, 9 Oct 2025 11:57:50 -0500 Subject: [PATCH 101/578] alerting docs: ai templating .md swap (#112233) moving the file to the proper templating section --- .../alerting-rules/templates/_index.md | 18 ------------------ .../template-notifications/_index.md | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/sources/alerting/alerting-rules/templates/_index.md b/docs/sources/alerting/alerting-rules/templates/_index.md index f66781f01e5..a2afde85de7 100644 --- a/docs/sources/alerting/alerting-rules/templates/_index.md +++ b/docs/sources/alerting/alerting-rules/templates/_index.md @@ -220,24 +220,6 @@ To preview label values, select `Use notification policy`, and then click on `Pr {{< figure src="/media/docs/alerting/alert-instance-routing-preview.png" max-width="1200px" alt="Routing preview displays label values" >}} -## Grafana Cloud AI-generated templates - -Grafana Cloud users can use built-in AI tool to generate templates in the appropriate [alerting template language](/docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/language/) for you. - -To use AI to create your template, follow these steps: - -1. Go to **Alerting -> Contact points**. - -1. Click the Notification Templates tab then, click the **+ Add notification template group** button. - -1. Name your template. - -1. In the Template group section, click the **Generate with AI** button. - -1. Supply the AI tool with a prompt or select from one of the example prompts and edit that if necessary. - -1. Click **Save**. - ## More information For further details on how to template alert rules, refer to: diff --git a/docs/sources/alerting/configure-notifications/template-notifications/_index.md b/docs/sources/alerting/configure-notifications/template-notifications/_index.md index 0eb2ecb210d..75b110a9764 100644 --- a/docs/sources/alerting/configure-notifications/template-notifications/_index.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/_index.md @@ -101,6 +101,24 @@ The notification template is assigned to the contact point to determine the noti By default, Grafana provides default templates, such as `{{define "default.title"}}` and `{{define "default.message"}}`, to format notification messages. +## Grafana Cloud AI-generated templates + +Grafana Cloud users can use built-in AI tool to generate templates in the appropriate [alerting template language](/docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/language/) for you. + +To use AI to create your template, follow these steps: + +1. Go to **Alerting -> Contact points**. + +1. Click the Notification Templates tab then, click the **+ Add notification template group** button. + +1. Name your template. + +1. In the Template group section, click the **Generate with AI** button. + +1. Supply the AI tool with a prompt or select from one of the example prompts and edit that if necessary. + +1. Click **Save**. + ## More information For further details on how to write notification templates, refer to: From 5eb295d8501cc80986ffed74e22bdae666fbc204 Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Thu, 9 Oct 2025 12:04:04 -0500 Subject: [PATCH 102/578] Docs: Adding new entry to 12.2 whats new (#112234) --- docs/sources/whatsnew/whats-new-in-v12-2.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/sources/whatsnew/whats-new-in-v12-2.md b/docs/sources/whatsnew/whats-new-in-v12-2.md index bc8314d56be..64951665275 100644 --- a/docs/sources/whatsnew/whats-new-in-v12-2.md +++ b/docs/sources/whatsnew/whats-new-in-v12-2.md @@ -51,6 +51,9 @@ posts: - title: Authentication and authorization items: - whats-new/2025-09-10-scim-configuration-ui.md + - title: Auditing + items: + - whats-new/2025-10-09-auditing-options-for-recording-data-sources-queries.md whats_new_grafana_version: 12.2 weight: -51 --- From 4d3c5d155009218dc22c93fd62c9a817be05097d Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:14:02 -0600 Subject: [PATCH 103/578] Dashboard Migrations: V13 no-op, remove 28 and v24 as they are autoMigrations; and remove dead code from DashboardMigrator (#110008) * migrate to v19 * migrate to v18 * Migration to be verified: v17 Convert minSpan to maxPerRow in panels * Migration to be verified: 16 Grid layout migration * Refactor v17 and v19 migrations to use shared helper functions * Migration to be verified: 15 No-op migration for schema consistency * Migration to be verified: 14 Shared crosshair to graph tooltip migration * cleanup * wip * complete migration * fix lint issues * refactor and test with minimal graph config * update tests * extract defaults outside the func * lint * lint * add missing showValues prop * add context and fix latest version * generate snapshots * v13 should be no-op * clean up * remove v28 * remove singlestat migraiton from frontend migrator because this is an automigration * remove unused function * Remove v24 table plugin logic * cleanup * remove plugin version for automigrate as it was used only in v24 and v28 that have been removed * cleanup --------- Co-authored-by: Dominik Prokop --- .../pkg/migration/frontend_defaults.go | 69 +- .../pkg/migration/frontend_defaults_test.go | 304 +++++ apps/dashboard/pkg/migration/migrate.go | 4 + .../schemaversion/angular_migration.go | 89 -- .../pkg/migration/schemaversion/migrations.go | 6 +- .../pkg/migration/schemaversion/v13.go | 11 + .../pkg/migration/schemaversion/v13_test.go | 38 + .../pkg/migration/schemaversion/v24.go | 626 +-------- .../pkg/migration/schemaversion/v24_test.go | 10 + .../pkg/migration/schemaversion/v28.go | 842 +------------ .../pkg/migration/schemaversion/v28_test.go | 257 ++-- .../datasource-mssql/mssql_fakedata.v42.json | 95 +- .../datasource-mssql/mssql_unittest.v42.json | 448 +++---- .../datasource-mysql/mysql_fakedata.v42.json | 96 +- .../datasource-mysql/mysql_unittest.v42.json | 448 +++---- .../postgres_fakedata.v42.json | 96 +- .../postgres_unittest.v42.json | 448 +++---- ...stdata-nested-variables-drilldown.v42.json | 106 +- .../testdata-nested-variables.v42.json | 95 +- .../panel-common/lazy_loading.v42.json | 412 +++--- .../panels_without_title.v42.json | 423 ++++--- .../panel-table/table_tests.v42.json | 1107 ++++++---------- .../testdata/input/v13.graph_thresholds.json | 62 + .../input/v13.minimal_graph_config.json | 34 + .../v13.graph_thresholds.v42.json | 174 +++ .../v13.minimal_graph_config.v42.json | 61 + .../v15.no-op-migration.v42.json | 57 +- .../v16.grid_layout_upgrade.v42.json | 38 +- .../v17.minspan_to_maxperrow.v42.json | 39 +- .../latest_version/v19.panel_links.v42.json | 38 +- .../v22.table_panel_align.v42.json | 70 +- .../latest_version/v24.table-angular.v42.json | 1116 ++++------------- ...inglestat_and_variable_properties.v42.json | 208 +-- .../v28.singlestat_migration.v42.json | 357 ++---- .../v13.graph_thresholds.v13.json | 98 ++ .../v13.minimal_graph_config.v13.json | 56 + .../single_version/v24.table-angular.v24.json | 1108 ++++------------ ...inglestat_and_variable_properties.v28.json | 208 +-- .../v28.singlestat_migration.v28.json | 357 ++---- eslint-suppressions.json | 4 +- .../dashboard/state/DashboardMigrator.ts | 116 +- .../DashboardMigratorSingleVersion.test.ts | 4 - ...ardMigratorToBackend.devDashboards.test.ts | 9 +- .../state/DashboardMigratorToBackend.test.ts | 4 - .../state/__tests__/migrationTestUtils.ts | 73 -- 45 files changed, 3555 insertions(+), 6766 deletions(-) delete mode 100644 apps/dashboard/pkg/migration/schemaversion/angular_migration.go create mode 100644 apps/dashboard/pkg/migration/schemaversion/v13.go create mode 100644 apps/dashboard/pkg/migration/schemaversion/v13_test.go create mode 100644 apps/dashboard/pkg/migration/testdata/input/v13.graph_thresholds.json create mode 100644 apps/dashboard/pkg/migration/testdata/input/v13.minimal_graph_config.json create mode 100644 apps/dashboard/pkg/migration/testdata/output/latest_version/v13.graph_thresholds.v42.json create mode 100644 apps/dashboard/pkg/migration/testdata/output/latest_version/v13.minimal_graph_config.v42.json create mode 100644 apps/dashboard/pkg/migration/testdata/output/single_version/v13.graph_thresholds.v13.json create mode 100644 apps/dashboard/pkg/migration/testdata/output/single_version/v13.minimal_graph_config.v13.json diff --git a/apps/dashboard/pkg/migration/frontend_defaults.go b/apps/dashboard/pkg/migration/frontend_defaults.go index 6f4cbbf87a4..76914a1b03a 100644 --- a/apps/dashboard/pkg/migration/frontend_defaults.go +++ b/apps/dashboard/pkg/migration/frontend_defaults.go @@ -549,6 +549,7 @@ func cleanupPanelForSaveWithContext(panel map[string]interface{}, isNested bool) // Clean up internal markers delete(panel, "_originallyHadTransformations") + delete(panel, "_originallyHadFieldConfigCustom") } // filterDefaultValues removes properties that match the default values (matches frontend's isEqual logic) @@ -600,8 +601,21 @@ func filterDefaultValues(panel map[string]interface{}, originalProperties map[st for prop, defaultValue := range defaults { if panelValue, exists := panel[prop]; exists { if isEqual(panelValue, defaultValue) { - // Special case: fieldConfig is always removed if it matches defaults (frontend getSaveModel behavior) + // Special case: fieldConfig - handle preservation of original custom object first if prop == "fieldConfig" { + // Check if we need to preserve the custom object before removing fieldConfig + if panel["_originallyHadFieldConfigCustom"] == true { + // Ensure fieldConfig structure exists with custom object + if fieldConfig, ok := panelValue.(map[string]interface{}); ok { + if defaults, ok := fieldConfig["defaults"].(map[string]interface{}); ok { + if _, hasCustom := defaults["custom"]; !hasCustom { + defaults["custom"] = map[string]interface{}{} + } + // Don't remove fieldConfig if we added custom back + continue + } + } + } delete(panel, prop) } else { // Only remove if it wasn't originally present in the input @@ -616,12 +630,32 @@ func filterDefaultValues(panel map[string]interface{}, originalProperties map[st // Remove empty targets arrays (frontend removes them in cleanup) removeIfDefaultValue(panel, "targets", []interface{}{}) + // Handle case where fieldConfig was removed but originally had custom object + if panel["_originallyHadFieldConfigCustom"] == true { + if _, hasFieldConfig := panel["fieldConfig"]; !hasFieldConfig { + // Recreate fieldConfig with custom object + panel["fieldConfig"] = map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{}, + }, + "overrides": []interface{}{}, + } + } + } + // Clean up fieldConfig to match frontend behavior if fieldConfig, exists := panel["fieldConfig"].(map[string]interface{}); exists { // Clean up fieldConfig defaults to match frontend behavior if defaults, hasDefaults := fieldConfig["defaults"].(map[string]interface{}); hasDefaults { // Remove properties that frontend considers as defaults and omits cleanupFieldConfigDefaults(defaults, panel) + + // Preserve custom object if it was originally present, even if empty + if panel["_originallyHadFieldConfigCustom"] == true { + if _, hasCustom := defaults["custom"]; !hasCustom { + defaults["custom"] = map[string]interface{}{} + } + } } } } @@ -1085,3 +1119,36 @@ func trackPanelOriginalTransformations(panel map[string]interface{}) { } } } + +// trackOriginalFieldConfigCustom marks panels that had fieldConfig.defaults.custom in the original input +// This is needed to match frontend hasOwnProperty behavior and preserve empty custom objects +func trackOriginalFieldConfigCustom(dashboard map[string]interface{}) { + if panels, ok := dashboard["panels"].([]interface{}); ok { + for _, panelInterface := range panels { + if panel, ok := panelInterface.(map[string]interface{}); ok { + trackPanelOriginalFieldConfigCustom(panel) + } + } + } +} + +// trackPanelOriginalFieldConfigCustom recursively tracks fieldConfig.defaults.custom in panels and nested panels +func trackPanelOriginalFieldConfigCustom(panel map[string]interface{}) { + // Mark if this panel had fieldConfig.defaults.custom in original input + if fieldConfig, ok := panel["fieldConfig"].(map[string]interface{}); ok { + if defaults, ok := fieldConfig["defaults"].(map[string]interface{}); ok { + if _, hasCustom := defaults["custom"]; hasCustom { + panel["_originallyHadFieldConfigCustom"] = true + } + } + } + + // Handle nested panels in row panels + if nestedPanels, ok := panel["panels"].([]interface{}); ok { + for _, nestedPanelInterface := range nestedPanels { + if nestedPanel, ok := nestedPanelInterface.(map[string]interface{}); ok { + trackPanelOriginalFieldConfigCustom(nestedPanel) + } + } + } +} diff --git a/apps/dashboard/pkg/migration/frontend_defaults_test.go b/apps/dashboard/pkg/migration/frontend_defaults_test.go index b17a941164a..5bc126eacec 100644 --- a/apps/dashboard/pkg/migration/frontend_defaults_test.go +++ b/apps/dashboard/pkg/migration/frontend_defaults_test.go @@ -807,6 +807,193 @@ func TestTransformationsArrayContextAwareLogic(t *testing.T) { } } +func TestTrackOriginalFieldConfigCustom(t *testing.T) { + tests := []struct { + name string + input map[string]interface{} + expected map[string]interface{} + }{ + { + name: "track_top_level_panel_with_fieldConfig_custom", + input: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": 1, + "type": "timeseries", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{}, + }, + "overrides": []interface{}{}, + }, + }, + map[string]interface{}{ + "id": 2, + "type": "table", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{}, + "overrides": []interface{}{}, + }, + }, + map[string]interface{}{ + "id": 3, + "type": "stat", + "title": "Panel without fieldConfig", + }, + }, + }, + expected: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": 1, + "type": "timeseries", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{}, + }, + "overrides": []interface{}{}, + }, + "_originallyHadFieldConfigCustom": true, // marker added + }, + map[string]interface{}{ + "id": 2, + "type": "table", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{}, + "overrides": []interface{}{}, + }, + // no marker added - no custom object + }, + map[string]interface{}{ + "id": 3, + "type": "stat", + "title": "Panel without fieldConfig", + // no marker added - no fieldConfig + }, + }, + }, + }, + { + name: "track_nested_panels_in_row_with_fieldConfig_custom", + input: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": 1, + "type": "row", + "title": "Row Panel", + "panels": []interface{}{ + map[string]interface{}{ + "id": 10, + "type": "singlestat", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{}, + }, + "overrides": []interface{}{}, + }, + }, + map[string]interface{}{ + "id": 11, + "type": "graph", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "unit": "bytes", + }, + "overrides": []interface{}{}, + }, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": 1, + "type": "row", + "title": "Row Panel", + "panels": []interface{}{ + map[string]interface{}{ + "id": 10, + "type": "singlestat", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{}, + }, + "overrides": []interface{}{}, + }, + "_originallyHadFieldConfigCustom": true, // marker added to nested panel + }, + map[string]interface{}{ + "id": 11, + "type": "graph", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "unit": "bytes", + }, + "overrides": []interface{}{}, + }, + // no marker added - no custom object + }, + }, + }, + }, + }, + }, + { + name: "track_panels_with_non_empty_custom", + input: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": 1, + "type": "timeseries", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{ + "axisPlacement": "left", + }, + }, + "overrides": []interface{}{}, + }, + }, + }, + }, + expected: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": 1, + "type": "timeseries", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{ + "axisPlacement": "left", + }, + }, + "overrides": []interface{}{}, + }, + "_originallyHadFieldConfigCustom": true, // marker added for non-empty custom + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a deep copy of input for testing + dashboard := deepCopy(tt.input).(map[string]interface{}) + + // Apply the tracking logic + trackOriginalFieldConfigCustom(dashboard) + + // Verify the result + if !compareValues(dashboard, tt.expected) { + t.Errorf("Test %s failed.\nExpected: %+v\nGot: %+v", tt.name, tt.expected, dashboard) + } + }) + } +} + func TestTrackOriginalTransformations(t *testing.T) { tests := []struct { name string @@ -915,6 +1102,123 @@ func TestTrackOriginalTransformations(t *testing.T) { } } +func TestCleanupPanelForSavePreservesOriginalFieldConfigCustom(t *testing.T) { + tests := []struct { + name string + input map[string]interface{} + expected map[string]interface{} + }{ + { + name: "preserve_empty_custom_when_originally_present", + input: map[string]interface{}{ + "type": "singlestat", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{}, // Empty but originally present + }, + "overrides": []interface{}{}, + }, + "_originallyHadFieldConfigCustom": true, // Marker indicating it was originally present + }, + expected: map[string]interface{}{ + "type": "stat", // Auto-migrated from singlestat + "autoMigrateFrom": "singlestat", // Auto-migration marker + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{}, // Should be preserved + }, + "overrides": []interface{}{}, + }, + }, + }, + { + name: "remove_empty_custom_when_not_originally_present", + input: map[string]interface{}{ + "type": "timeseries", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{}, // Empty and not originally present + }, + "overrides": []interface{}{}, + }, + // No marker - custom was not originally present + }, + expected: map[string]interface{}{ + "type": "timeseries", + // fieldConfig should be removed entirely as it matches defaults + }, + }, + { + name: "preserve_non_empty_custom_regardless_of_marker", + input: map[string]interface{}{ + "type": "table", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{ + "displayMode": "list", + }, + }, + "overrides": []interface{}{}, + }, + }, + expected: map[string]interface{}{ + "type": "table", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{ + "displayMode": "list", + }, + }, + "overrides": []interface{}{}, + }, + }, + }, + { + name: "add_custom_when_originally_present_but_missing", + input: map[string]interface{}{ + "type": "stat", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{}, // custom missing + "overrides": []interface{}{}, + }, + "_originallyHadFieldConfigCustom": true, // But marker indicates it was originally present + }, + expected: map[string]interface{}{ + "type": "stat", + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "custom": map[string]interface{}{}, // Should be added back + }, + "overrides": []interface{}{}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + panel := make(map[string]interface{}) + for k, v := range tt.input { + panel[k] = v + } + + cleanupPanelForSaveWithContext(panel, false) + + // Verify expected properties exist + for key, expectedValue := range tt.expected { + if actualValue, exists := panel[key]; !exists { + t.Errorf("Property %s should exist but is missing", key) + } else if !compareValues(actualValue, expectedValue) { + t.Errorf("Property %s has wrong value. Expected: %v, Got: %v", key, expectedValue, actualValue) + } + } + + // Verify internal markers are cleaned up + assertPropertyRemoved(t, panel, "_originallyHadFieldConfigCustom") + }) + } +} + func TestCleanupPanelForSave(t *testing.T) { tests := []struct { name string diff --git a/apps/dashboard/pkg/migration/migrate.go b/apps/dashboard/pkg/migration/migrate.go index 62a24db3f2c..73fdf460416 100644 --- a/apps/dashboard/pkg/migration/migrate.go +++ b/apps/dashboard/pkg/migration/migrate.go @@ -66,6 +66,10 @@ func (m *migrator) migrate(ctx context.Context, dash map[string]interface{}, tar // This is needed to match frontend hasOwnProperty behavior trackOriginalTransformations(dash) + // 1.1. Track which panels had fieldConfig.defaults.custom in original input + // This is needed to preserve empty custom objects that were originally present + trackOriginalFieldConfigCustom(dash) + // 2. Apply ALL frontend defaults FIRST (DashboardModel + PanelModel defaults) // This replicates the behavior of the frontend DashboardModel and PanelModel constructors applyFrontendDefaults(dash) diff --git a/apps/dashboard/pkg/migration/schemaversion/angular_migration.go b/apps/dashboard/pkg/migration/schemaversion/angular_migration.go deleted file mode 100644 index 7dee79d8c29..00000000000 --- a/apps/dashboard/pkg/migration/schemaversion/angular_migration.go +++ /dev/null @@ -1,89 +0,0 @@ -package schemaversion - -var notPersistedProperties = []string{ - "events", - "isViewing", - "isEditing", - "isInView", - "hasRefreshed", - "cachedPluginOptions", - "plugin", - "queryRunner", - "replaceVariables", - "configRev", - "hasSavedPanelEditChange", - "getDisplayTitle", - "dataSupport", - "key", - "isNew", - "refreshWhenInView", -} - -var mustKeepProperties = []string{ - "id", - "gridPos", - "type", - "title", - "scopedVars", - "repeat", - "repeatPanelId", - "repeatDirection", - "repeatedByRow", - "minSpan", - "collapsed", - "panels", - "targets", - "datasource", - "timeFrom", - "timeShift", - "hideTimeOverride", - "description", - "links", - "fullscreen", - "isEditing", - "isViewing", - "hasRefreshed", - "events", - "cacheTimeout", - "queryCachingTTL", - "cachedPluginOptions", - "transparent", - "pluginVersion", - "queryRunner", - "transformations", - "fieldConfig", - "maxDataPoints", - "interval", - "replaceVariables", - "libraryPanel", - "getDisplayTitle", - "configRev", - "key", -} - -// getOptionsToRemember returns a map of panel properties that should be remembered -// during panel type changes, excluding notPersistedProperties and mustKeepProperties -func getOptionsToRemember(panel map[string]interface{}) map[string]interface{} { - // Create sets for faster lookup - notPersistedSet := make(map[string]bool) - for _, prop := range notPersistedProperties { - notPersistedSet[prop] = true - } - - mustKeepSet := make(map[string]bool) - for _, prop := range mustKeepProperties { - mustKeepSet[prop] = true - } - - // Filter the panel properties - result := make(map[string]interface{}) - for key, value := range panel { - // Skip properties that are in notPersistedProperties or mustKeepProperties - if notPersistedSet[key] || mustKeepSet[key] { - continue - } - result[key] = value - } - - return result -} diff --git a/apps/dashboard/pkg/migration/schemaversion/migrations.go b/apps/dashboard/pkg/migration/schemaversion/migrations.go index e8d50a9aecb..df8d1030ffb 100644 --- a/apps/dashboard/pkg/migration/schemaversion/migrations.go +++ b/apps/dashboard/pkg/migration/schemaversion/migrations.go @@ -7,11 +7,8 @@ import ( ) const ( - MIN_VERSION = 13 + MIN_VERSION = 12 LATEST_VERSION = 42 - - // The pluginVersion to set after simulating auto-migrate for angular panels - pluginVersionForAutoMigrate = "12.1.0" ) type SchemaVersionMigrationFunc func(context.Context, map[string]interface{}) error @@ -38,6 +35,7 @@ type PanelPluginInfo struct { func GetMigrations(dsInfoProvider DataSourceInfoProvider) map[int]SchemaVersionMigrationFunc { return map[int]SchemaVersionMigrationFunc{ + 13: V13, 14: V14, 15: V15, 16: V16, diff --git a/apps/dashboard/pkg/migration/schemaversion/v13.go b/apps/dashboard/pkg/migration/schemaversion/v13.go new file mode 100644 index 00000000000..791c3576022 --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v13.go @@ -0,0 +1,11 @@ +package schemaversion + +import ( + "context" +) + +// V13 is a no-op migration +func V13(_ context.Context, dashboard map[string]interface{}) error { + dashboard["schemaVersion"] = 13 + return nil +} diff --git a/apps/dashboard/pkg/migration/schemaversion/v13_test.go b/apps/dashboard/pkg/migration/schemaversion/v13_test.go new file mode 100644 index 00000000000..729bcb25120 --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v13_test.go @@ -0,0 +1,38 @@ +package schemaversion_test + +import ( + "testing" + + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" +) + +func TestV13(t *testing.T) { + tests := []migrationTestCase{ + { + name: "v13 no-op migration, updates schema version only", + input: map[string]interface{}{ + "title": "V13 No-Op Migration Test Dashboard", + "schemaVersion": 12, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "title": "Panel remains unchanged", + "id": 1, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V13 No-Op Migration Test Dashboard", + "schemaVersion": 13, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "title": "Panel remains unchanged", + "id": 1, + }, + }, + }, + }, + } + runMigrationTests(t, tests, schemaversion.V13) +} diff --git a/apps/dashboard/pkg/migration/schemaversion/v24.go b/apps/dashboard/pkg/migration/schemaversion/v24.go index 4526e23d15e..d9b4dd59ed4 100644 --- a/apps/dashboard/pkg/migration/schemaversion/v24.go +++ b/apps/dashboard/pkg/migration/schemaversion/v24.go @@ -2,189 +2,59 @@ package schemaversion import ( "context" - "strconv" ) -// V24 migration migrates the angular table panel to the standard table panel -// In the frontend, this is an auto-migration meaning that this angular panel is always migrated to table panel. -// The backend replicates the complete frontend auto-migration logic since it cannot rely on frontend auto-migration. -// -// This migration performs: -// 1. Converts 'styles' array to 'fieldConfig' with 'defaults' and 'overrides' -// 2. Migrates thresholds and colors to new threshold format -// 3. Converts column-specific styles to field overrides -// 4. Migrates transformations from old format to new transformation system -// 5. Handles various style properties: unit, decimals, alignment, color modes, links, date formatting, hidden columns -// 6. Removes deprecated properties: styles, transform, columns +// V24 migration handles setting autoMigrateFrom +// This is a hacky way that matches frontend's logic +// For reason see https://github.com/grafana/grafana/pull/102146 +// The issue is that if panel is "table" and it has styles, it should be migrated to "table-old" -// Example 1: Basic table with defaults -// Before migration: +// Example before migration: // { -// "panels": [ -// { -// "id": 1, -// "type": "table", -// "title": "Basic Table", -// "styles": [ -// { -// "pattern": "/.*/", -// "thresholds": ["10", "20", "30"], -// "colors": ["green", "yellow", "red"], -// "unit": "bytes", -// "decimals": 2 -// } -// ], -// "targets": [{ "refId": "A" }] -// } -// ] +// "id": 4, +// "type": "table", +// "title": "Table with Timeseries to Rows Transform", +// "description": "Tests migration of timeseries_to_rows transform to seriesToRows transformation.", +// "styles": [ +// { +// "pattern": "/.*/", +// "unit": "short" +// } +// ], +// "transform": "timeseries_to_rows", +// "targets": [{ "refId": "A" }] // } // -// After migration: +// Example after migration: // { -// "panels": [ -// { -// "id": 1, -// "type": "table", -// "title": "Basic Table", -// "fieldConfig": { -// "defaults": { -// "unit": "bytes", -// "decimals": 2, -// "custom": {}, -// "thresholds": { -// "mode": "absolute", -// "steps": [ -// { "color": "green", "value": null }, -// { "color": "green", "value": 10 }, -// { "color": "yellow", "value": 20 }, -// { "color": "red", "value": 30 } -// ] -// } -// }, -// "overrides": [] -// }, -// "transformations": [], -// "targets": [{ "refId": "A" }], -// "pluginVersion": "{current_grafana_version}" -// } -// ] -// } - -// Example 2: Complex table with overrides and transformations -// Before migration: -// { -// "panels": [ -// { -// "id": 2, -// "type": "table", -// "title": "Complex Table", -// "styles": [ -// { -// "pattern": "/.*/", -// "unit": "percent", -// "align": "center", -// "colorMode": "cell" -// }, -// { -// "pattern": "Status", -// "alias": "Current Status", -// "colorMode": "value", -// "align": "left" -// }, -// { -// "pattern": "/Error.*/", -// "link": true, -// "linkUrl": "http://example.com/errors", -// "linkTooltip": "View errors", -// "linkTargetBlank": true -// }, -// { -// "pattern": "Time", -// "type": "date", -// "dateFormat": "YYYY-MM-DD HH:mm:ss", -// "alias": "Timestamp" -// }, -// { -// "pattern": "Hidden", -// "type": "hidden" -// } -// ], -// "transform": "timeseries_aggregations", -// "columns": [ -// { "value": "avg", "text": "Average" }, -// { "value": "max", "text": "Maximum" } -// ], -// "targets": [{ "refId": "A" }] -// } -// ] -// } -// -// After migration: -// { -// "panels": [ -// { -// "id": 2, -// "type": "table", -// "title": "Complex Table", -// "fieldConfig": { -// "defaults": { -// "unit": "percent", -// "custom": { -// "align": "center", -// "cellOptions": { "type": "color-background" } -// } -// }, -// "overrides": [ -// { -// "matcher": { "id": "byName", "options": "Status" }, -// "properties": [ -// { "id": "displayName", "value": "Current Status" }, -// { "id": "custom.cellOptions", "value": { "type": "color-text" } }, -// { "id": "custom.align", "value": "left" } -// ] -// }, -// { -// "matcher": { "id": "byRegexp", "options": "/Error.*/" }, -// "properties": [ -// { -// "id": "links", -// "value": [{ -// "title": "View errors", -// "url": "http://example.com/errors", -// "targetBlank": true -// }] -// } -// ] -// }, -// { -// "matcher": { "id": "byName", "options": "Time" }, -// "properties": [ -// { "id": "displayName", "value": "Timestamp" }, -// { "id": "unit", "value": "time: YYYY-MM-DD HH:mm:ss" } -// ] -// }, -// { -// "matcher": { "id": "byName", "options": "Hidden" }, -// "properties": [ -// { "id": "custom.hideFrom.viz", "value": true } -// ] -// } -// ] -// }, -// "transformations": [ -// { -// "id": "reduce", -// "options": { -// "reducers": ["mean", "max"], -// "includeTimeField": false -// } -// } -// ], -// "targets": [{ "refId": "A" }], -// "pluginVersion": "{current_grafana_version}" -// } -// ] -// } +// "autoMigrateFrom": "table-old", +// "datasource": { +// "apiVersion": "v1", +// "type": "prometheus", +// "uid": "default-ds-uid" +// }, +// "description": "Tests migration of timeseries_to_rows transform to seriesToRows transformation.", +// "id": 4, +// "styles": [ +// { +// "pattern": "/.*/", +// "unit": "short" +// } +// ], +// "targets": [ +// { +// "datasource": { +// "apiVersion": "v1", +// "type": "prometheus", +// "uid": "default-ds-uid" +// }, +// "refId": "A" +// } +// ], +// "title": "Table with Timeseries to Rows Transform", +// "transform": "timeseries_to_rows", +// "type": "table" +// } func V24(_ context.Context, dashboard map[string]interface{}) error { dashboard["schemaVersion"] = 24 @@ -211,406 +81,18 @@ func V24(_ context.Context, dashboard map[string]interface{}) error { continue } - // The grafana version that matches the hardcoded autoMigrate plugins - panelMap["pluginVersion"] = pluginVersionForAutoMigrate - err := tablePanelChangedHandler(panelMap) - if err != nil { - return err - } - } - - return nil -} - -func tablePanelChangedHandler(panel map[string]interface{}) error { - prevOptions := getOptionsToRemember(panel) - - transformations := migrateTransformations(panel, prevOptions) - - prevDefaults := findDefaultStyle(prevOptions) - defaults := migrateDefaults(prevDefaults) - - overrides := findNonDefaultStyles(prevOptions) - - if len(overrides) == 0 { - overrides = []interface{}{} - } - - // Only add transformations if they're not empty - frontend omits empty arrays - if len(transformations) > 0 { - panel["transformations"] = transformations - } - panel["fieldConfig"] = map[string]interface{}{ - "defaults": defaults, - "overrides": overrides, - } - - // Add minimal table panel options to match frontend behavior - // Frontend doesn't add default footer options, so we don't either - panel["options"] = map[string]interface{}{ - "cellHeight": "sm", - "showHeader": true, - } - - // Remove deprecated properties - delete(panel, "styles") - delete(panel, "transform") - delete(panel, "columns") - - // Remove legend property - frontend table panel migration doesn't preserve it - delete(panel, "legend") - - return nil -} - -// findDefaultStyle finds the style with pattern '/.*/' (default style) -func findDefaultStyle(prevOptions map[string]interface{}) map[string]interface{} { - if styles, ok := prevOptions["styles"].([]interface{}); ok { - for _, style := range styles { - if styleMap, ok := style.(map[string]interface{}); ok { - if pattern, ok := styleMap["pattern"].(string); ok && pattern == "/.*/" { - return styleMap - } - } - } - } - return nil -} - -// findNonDefaultStyles finds all styles that don't have pattern '/.*/' -func findNonDefaultStyles(prevOptions map[string]interface{}) []interface{} { - var overrides []interface{} - - if styles, ok := prevOptions["styles"].([]interface{}); ok { - for _, style := range styles { - if styleMap, ok := style.(map[string]interface{}); ok { - if pattern, ok := styleMap["pattern"].(string); ok && pattern != "/.*/" { - override := migrateTableStyleToOverride(styleMap) - overrides = append(overrides, override) - } - } - } - } - return overrides -} - -// migrateTransformations converts old table transformations to new format -func migrateTransformations(panel map[string]interface{}, oldOpts map[string]interface{}) []interface{} { - transformations := []interface{}{} - if existing, ok := panel["transformations"].([]interface{}); ok { - transformations = existing - } - - // Check if oldOpts has a transform that we can map - if transform, ok := oldOpts["transform"].(string); ok { - if newTransformID, exists := transformsMap[transform]; exists { - opts := map[string]interface{}{ - "reducers": []interface{}{}, - } - - // Handle timeseries_aggregations specifically - if transform == "timeseries_aggregations" { - opts["includeTimeField"] = false - - // Map columns to reducers - if columns, ok := oldOpts["columns"].([]interface{}); ok { - var reducers []interface{} - for _, column := range columns { - if columnMap, ok := column.(map[string]interface{}); ok { - if value, ok := columnMap["value"].(string); ok { - if reducer, exists := columnsMap[value]; exists { - reducers = append(reducers, reducer) - } - } - } - } - opts["reducers"] = reducers - } - } - - // Add the transformation - transformation := map[string]interface{}{ - "id": newTransformID, - "options": opts, - } - transformations = append(transformations, transformation) - } - } - - return transformations -} - -// transformsMap maps old transform names to new transformation IDs -var transformsMap = map[string]string{ - "timeseries_to_rows": "seriesToRows", - "timeseries_to_columns": "seriesToColumns", - "timeseries_aggregations": "reduce", - "table": "merge", -} - -// columnsMap maps old column values to new reducer names -var columnsMap = map[string]string{ - "avg": "mean", - "min": "min", - "max": "max", - "total": "sum", - "current": "lastNotNull", - "count": "count", -} - -// migrateTableStyleToOverride converts a table style to a field config override -func migrateTableStyleToOverride(style map[string]interface{}) map[string]interface{} { - pattern, _ := style["pattern"].(string) - - // Determine field matcher ID based on pattern - fieldMatcherID := "byName" - if pattern != "" && len(pattern) >= 2 && pattern[0] == '/' && pattern[len(pattern)-1] == '/' { - fieldMatcherID = "byRegexp" - } - - override := map[string]interface{}{ - "matcher": map[string]interface{}{ - "id": fieldMatcherID, - "options": pattern, - }, - "properties": []interface{}{}, - } - - properties := override["properties"].([]interface{}) - - // Add display name - if alias, ok := style["alias"].(string); ok && alias != "" { - properties = append(properties, map[string]interface{}{ - "id": "displayName", - "value": alias, - }) - } - - // Add unit - if unit, ok := style["unit"].(string); ok && unit != "" { - properties = append(properties, map[string]interface{}{ - "id": "unit", - "value": unit, - }) - } - - // Add decimals - if decimals := GetIntValue(style, "decimals", -1); decimals != -1 { - properties = append(properties, map[string]interface{}{ - "id": "decimals", - "value": decimals, - }) - } - - // Handle date type - if styleType, ok := style["type"].(string); ok && styleType == "date" { - if dateFormat, ok := style["dateFormat"].(string); ok { - properties = append(properties, map[string]interface{}{ - "id": "unit", - "value": "time: " + dateFormat, - }) - } - } - - // Handle hidden type - if styleType, ok := style["type"].(string); ok && styleType == "hidden" { - properties = append(properties, map[string]interface{}{ - "id": "custom.hideFrom.viz", - "value": true, - }) - } - - // Handle links - if link, ok := style["link"].(bool); ok && link { - linkTooltip, _ := style["linkTooltip"].(string) - linkUrl, _ := style["linkUrl"].(string) - linkTargetBlank, _ := style["linkTargetBlank"].(bool) - - properties = append(properties, map[string]interface{}{ - "id": "links", - "value": []interface{}{ - map[string]interface{}{ - "title": linkTooltip, - "url": linkUrl, - "targetBlank": linkTargetBlank, - }, - }, - }) - } - - // Handle color mode - if colorMode, ok := style["colorMode"].(string); ok && colorMode != "" { - if newColorMode, exists := colorModeMap[colorMode]; exists { - properties = append(properties, map[string]interface{}{ - "id": "custom.cellOptions", - "value": map[string]interface{}{ - "type": newColorMode, - }, - }) - } - } - - // Handle alignment - if align, ok := style["align"].(string); ok && align != "" { - var alignValue interface{} - if align == "auto" { - alignValue = nil // Frontend sets to null and filters it out + var currentType string + if wasAngularTable { + currentType = "table-old" } else { - alignValue = align + currentType = "table" } - properties = append(properties, map[string]interface{}{ - "id": "custom.align", - "value": alignValue, - }) - } - // Handle thresholds - if thresholds, ok := style["thresholds"].([]interface{}); ok && len(thresholds) > 0 { - if colors, ok := style["colors"].([]interface{}); ok && len(colors) > 0 { - steps := generateThresholds(thresholds, colors) - properties = append(properties, map[string]interface{}{ - "id": "thresholds", - "value": map[string]interface{}{ - "mode": "absolute", - "steps": steps, - }, - }) + if currentType == "table-old" { + panelMap["autoMigrateFrom"] = "table-old" + panelMap["type"] = "table" } } - override["properties"] = properties - return override -} - -// migrateDefaults converts default table styles to field config defaults -func migrateDefaults(prevDefaults map[string]interface{}) map[string]interface{} { - defaults := map[string]interface{}{ - "custom": map[string]interface{}{ - "align": "auto", - "cellOptions": map[string]interface{}{ - "type": "auto", - }, - "inspect": false, - "footer": map[string]interface{}{ - "reducers": []interface{}{}, - }, - }, - "mappings": []interface{}{}, - } - - // Add default thresholds for all table panels to match frontend behavior - // The frontend applies the table panel's default field config which includes thresholds - hasThresholds := false - if prevDefaults != nil { - if thresholds, ok := prevDefaults["thresholds"].([]interface{}); ok && len(thresholds) > 0 { - hasThresholds = true - } - } - - // Add default thresholds for all table panels (when prevDefaults exists) without existing thresholds - if !hasThresholds { - defaults["thresholds"] = map[string]interface{}{ - "mode": "absolute", - "steps": []interface{}{ - map[string]interface{}{"color": "green", "value": (*float64)(nil)}, - map[string]interface{}{"color": "red", "value": 80}, - }, - } - } - - if prevDefaults == nil { - return defaults - } - - if unit := GetStringValue(prevDefaults, "unit"); unit != "" { - defaults["unit"] = unit - } - - if decimals := GetIntValue(prevDefaults, "decimals", -1); decimals != -1 { - defaults["decimals"] = decimals - } - - if alias, ok := prevDefaults["alias"].(string); ok { - defaults["displayName"] = alias - } - - if align, ok := prevDefaults["align"].(string); ok && align != "" { - var alignValue interface{} - if align == "auto" { - alignValue = nil // Frontend sets to null and filters it out - } else { - alignValue = align - } - defaults["custom"].(map[string]interface{})["align"] = alignValue - } - - if thresholds, ok := prevDefaults["thresholds"].([]interface{}); ok && len(thresholds) > 0 { - if colors, ok := prevDefaults["colors"].([]interface{}); ok && len(colors) > 0 { - steps := generateThresholds(thresholds, colors) - defaults["thresholds"] = map[string]interface{}{ - "mode": "absolute", - "steps": steps, - } - } - } - - if colorMode, ok := prevDefaults["colorMode"].(string); ok && colorMode != "" { - if newColorMode, exists := colorModeMap[colorMode]; exists { - defaults["custom"].(map[string]interface{})["cellOptions"] = map[string]interface{}{ - "type": newColorMode, - } - } - } - - return defaults -} - -func generateThresholds(thresholds []interface{}, colors []interface{}) []interface{} { - steps := []interface{}{} - - // Add the base step (equivalent to -Infinity) - var baseColor interface{} = "red" // default fallback - if len(colors) > 0 && colors[0] != nil { - baseColor = colors[0] - } - - steps = append(steps, map[string]interface{}{ - "color": baseColor, - "value": (*float64)(nil), - }) - - // Add threshold steps - for i, threshold := range thresholds { - var value float64 - switch v := threshold.(type) { - case string: - if parsed, err := strconv.ParseFloat(v, 64); err == nil { - value = parsed - } - case float64: - value = v - case int: - value = float64(v) - } - - step := map[string]interface{}{ - "value": value, - } - - // Only add color if there's a corresponding color in the colors array - // This matches the frontend behavior where colors[idx] might be undefined - if i+1 < len(colors) && colors[i+1] != nil { - step["color"] = colors[i+1] - } - - steps = append(steps, step) - } - - return steps -} - -var colorModeMap = map[string]string{ - "cell": "color-background", - "row": "color-background", - "value": "color-text", + return nil } diff --git a/apps/dashboard/pkg/migration/schemaversion/v24_test.go b/apps/dashboard/pkg/migration/schemaversion/v24_test.go index 45bbf0b1df2..9abec040221 100644 --- a/apps/dashboard/pkg/migration/schemaversion/v24_test.go +++ b/apps/dashboard/pkg/migration/schemaversion/v24_test.go @@ -44,6 +44,16 @@ func TestV24TablePanelMigration(t *testing.T) { }, description: "V24 migration should not add empty transformations arrays to table panels", }, + { + name: "migrate_table_old_to_table", + input: map[string]interface{}{ + "type": "table-old", + }, + expected: map[string]interface{}{ + "type": "table", + }, + description: "V24 migration should migrate table-old to table", + }, } for _, tt := range tests { diff --git a/apps/dashboard/pkg/migration/schemaversion/v28.go b/apps/dashboard/pkg/migration/schemaversion/v28.go index 2144ae65959..d1f3bab6e37 100644 --- a/apps/dashboard/pkg/migration/schemaversion/v28.go +++ b/apps/dashboard/pkg/migration/schemaversion/v28.go @@ -2,61 +2,32 @@ package schemaversion import ( "context" - "fmt" - "strconv" - "strings" ) -// V28 migrates singlestat panels to stat/gauge panels and removes deprecated variable properties. -// -// The migration performs two main tasks: -// 1. Migrates singlestat panels to either stat or gauge panels based on their configuration -// 2. Removes deprecated variable properties (tags, tagsQuery, tagValuesQuery, useTags) -// -// The migration includes comprehensive logic from the frontend: -// - Panel type migration (singlestat -> stat/gauge) -// - Field config migration with thresholds, mappings, and display options -// - Options migration including reduceOptions, orientation, and other panel-specific settings -// - Support for both angular singlestat and grafana-singlestat-panel migrations +// V28 removes deprecated variable properties (tags, tagsQuery, tagValuesQuery, useTags) // // Example before migration: // -// "panels": [ -// { -// "type": "singlestat", -// "gauge": { "show": true }, -// "targets": [{ "refId": "A" }] -// } -// ], -// "templating": { -// "list": [ -// { "name": "var1", "tags": ["tag1"], "tagsQuery": "query", "tagValuesQuery": "values", "useTags": true } -// ] +// { +// "templating": { +// "list": [ +// { "name": "var1", "tags": ["tag1"], "tagsQuery": "query", "tagValuesQuery": "values", "useTags": true } +// ] +// } // } // // Example after migration: // -// "panels": [ -// { -// "type": "gauge", -// "targets": [{ "refId": "A" }] -// } -// ], -// "templating": { -// "list": [ -// { "name": "var1" } -// ] +// { +// "templating": { +// "list": [ +// { "name": "var1" } +// ] +// } // } func V28(_ context.Context, dashboard map[string]interface{}) error { dashboard["schemaVersion"] = 28 - // Migrate singlestat panels - if panels, ok := dashboard["panels"].([]interface{}); ok { - if err := processPanels(panels); err != nil { - return err - } - } - // Remove deprecated variable properties if templating, ok := dashboard["templating"].(map[string]interface{}); ok { if list, ok := templating["list"].([]interface{}); ok { @@ -71,751 +42,6 @@ func V28(_ context.Context, dashboard map[string]interface{}) error { return nil } -func processPanels(panels []interface{}) error { - for _, panel := range panels { - p, ok := panel.(map[string]interface{}) - if !ok { - continue - } - - // Process nested panels if this is a row panel - if p["type"] == "row" { - if nestedPanels, ok := p["panels"].([]interface{}); ok { - if err := processPanels(nestedPanels); err != nil { - return err - } - } - continue - } - - // Migrate singlestat panels (including those already auto-migrated to stat) - if p["type"] == "singlestat" || p["type"] == "grafana-singlestat-panel" || - p["autoMigrateFrom"] == "singlestat" || p["autoMigrateFrom"] == "grafana-singlestat-panel" { - if err := migrateSinglestatPanel(p); err != nil { - return err - } - } - - // Note: Panel defaults (including options object) are already applied - // by applyPanelDefaults() in the main migration flow for ALL panels - // No need for stat-specific normalization - } - - return nil -} - -func migrateSinglestatPanel(panel map[string]interface{}) error { - targetType := "stat" - - // NOTE: The legacy types "singlestat" and "gauge" are both angular only - // This are not supported by any version that could run this migration, so there is - // no need to maintain a distinction or fallback to the non-stat version - - // NOTE: DashboardMigrator's migrateSinglestat function has some logic that never gets called - // migrateSinglestat will only run if (panel.type === 'singlestat') - // but this will not be the case because PanelModel runs restoreModel in the constructor - // and since singlestat is in the autoMigrateAngular map, it will be migrated to stat, - // and therefore migrateSinglestat will never run so this logic inside of it will never apply - // if ((panel as any).gauge?.show) { - // gaugePanelPlugin.meta = config.panels['gauge'] - // panel.changePlugin(gaugePanelPlugin) - - // Store original type for migration context (only for stat/gauge migration) - // Set autoMigrateFrom to track the original type for proper migration logic - originalType := panel["type"].(string) - // Only set autoMigrateFrom if it doesn't already exist (preserve frontend defaults) - if _, exists := panel["autoMigrateFrom"]; !exists { - panel["autoMigrateFrom"] = originalType - } - panel["type"] = targetType - panel["pluginVersion"] = pluginVersionForAutoMigrate - - // Migrate panel options and field config - migrateSinglestatOptions(panel, originalType) - - return nil -} - -// migrateSinglestatOptions handles the complete migration of singlestat panel options and field config -func migrateSinglestatOptions(panel map[string]interface{}, originalType string) { - // Preserve important panel-level properties that should not be removed - // These properties are preserved by the frontend's getSaveModel() method - var maxDataPoints interface{} - if mdp, exists := panel["maxDataPoints"]; exists { - maxDataPoints = mdp - } - - // Initialize field config if not present - if panel["fieldConfig"] == nil { - panel["fieldConfig"] = map[string]interface{}{ - "defaults": map[string]interface{}{}, - "overrides": []interface{}{}, - } - } - - fieldConfig := panel["fieldConfig"].(map[string]interface{}) - defaults := fieldConfig["defaults"].(map[string]interface{}) - - // Migrate from angular singlestat configuration using appropriate strategy - // Use autoMigrateFrom if available, otherwise use originalType - migrationType := originalType - if autoMigrateFrom, exists := panel["autoMigrateFrom"].(string); exists { - migrationType = autoMigrateFrom - } - - if migrationType == "grafana-singlestat-panel" { - migrateGrafanaSinglestatPanel(panel, defaults) - } else { - migratetSinglestat(panel, defaults) - } - - // Apply shared migration logic - applySharedSinglestatMigration(defaults) - - // Apply complete stat panel defaults (matches frontend getPanelOptionsWithDefaults) - // The frontend applies these defaults after migration via applyPluginOptionDefaults - applyCompleteStatPanelDefaults(panel) - - // Create proper fieldConfig structure from defaults - createFieldConfigFromDefaults(panel, defaults) - - // Restore preserved panel-level properties - if maxDataPoints != nil { - panel["maxDataPoints"] = maxDataPoints - } - - // Clean up old angular properties after migration - cleanupAngularProperties(panel) -} - -// getDefaultStatOptions returns the default options structure for stat panels -// This matches the frontend's stat panel defaultOptions exactly -func getDefaultStatOptions() map[string]interface{} { - // For now, return the explicit defaults until we integrate the centralized system - return map[string]interface{}{ - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "percentChangeColorMode": "standard", - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true, - "reduceOptions": map[string]interface{}{ - "calcs": []string{"lastNotNull"}, // Matches frontend: ReducerID.lastNotNull - "fields": "", - "values": false, - }, - "orientation": "auto", - } -} - -// migratetSinglestat handles explicit migration from 'singlestat' panels -// Based on frontend migrateFromAngularSinglestat function -func migratetSinglestat(panel map[string]interface{}, defaults map[string]interface{}) { - angularOpts := extractAngularOptions(panel) - - // Extract valueName for reducer mapping (matches frontend migrateFromAngularSinglestat) - var valueName string - if vn, ok := angularOpts["valueName"].(string); ok { - valueName = vn - } - - // Set calcs based on valueName (matches frontend: calcs: [reducer ? reducer.id : ReducerID.mean]) - var calcs []string - if reducer := getReducerForValueName(valueName); reducer != "" { - calcs = []string{reducer} - } else { - // Use mean as fallback (matches frontend migrateFromAngularSinglestat: ReducerID.mean) - calcs = []string{"mean"} - } - - // Create options exactly like frontend migrateFromAngularSinglestat - options := map[string]interface{}{ - "reduceOptions": map[string]interface{}{ - "calcs": calcs, - "fields": "", - "values": false, - }, - "orientation": "horizontal", // Matches frontend migrateFromAngularSinglestat: VizOrientation.Horizontal - } - - // Migrate thresholds FIRST (consolidated: both panel types create DEFAULT_THRESHOLDS for empty strings) - migrateThresholds(angularOpts, defaults) - - // If no thresholds were set from angular migration, add default stat panel thresholds - // This matches the behavior of frontend pluginLoaded which adds default thresholds - if _, hasThresholds := defaults["thresholds"]; !hasThresholds { - defaults["thresholds"] = map[string]interface{}{ - "mode": "absolute", - "steps": []interface{}{ - map[string]interface{}{ - "color": "green", - "value": (*float64)(nil), - }, - map[string]interface{}{ - "color": "red", - "value": 80, - }, - }, - } - } - - // Apply common angular option migrations (value mappings can now use threshold colors) - applyCommonAngularMigration(panel, defaults, options, angularOpts) - - // Merge new options with existing panel options to preserve properties like maxDataPoints - if existingOptions, exists := panel["options"].(map[string]interface{}); exists { - for key, value := range options { - existingOptions[key] = value - } - } else { - panel["options"] = options - } -} - -// migrateGrafanaSinglestatPanel handles auto-migration from 'grafana-singlestat-panel' -// Uses the same migration logic as singlestat panels since the frontend applies -// migrateFromAngularSinglestat to both panel types. -func migrateGrafanaSinglestatPanel(panel map[string]interface{}, defaults map[string]interface{}) { - migratetSinglestat(panel, defaults) -} - -// migrateThresholds handles threshold migration for both singlestat panel types -// Both panel types now create DEFAULT_THRESHOLDS when threshold string is empty (consolidated behavior) -func migrateThresholds(angularOpts map[string]interface{}, defaults map[string]interface{}) { - if thresholds, ok := angularOpts["thresholds"].(string); ok { - if colors, ok := angularOpts["colors"].([]interface{}); ok { - if thresholds != "" { - // Non-empty thresholds: use normal migration - migrateThresholdsAndColors(defaults, thresholds, colors) - } else { - // Empty thresholds: use frontend DEFAULT_THRESHOLDS fallback (both panel types) - defaults["thresholds"] = map[string]interface{}{ - "mode": "absolute", - "steps": []interface{}{ - map[string]interface{}{ - "color": "green", - "value": (*float64)(nil), // Use pointer to ensure field is present in JSON - }, - map[string]interface{}{ - "color": "red", - "value": 80, - }, - }, - } - } - } - } -} - -// applyCommonAngularMigration applies migrations common to both singlestat types -func applyCommonAngularMigration(panel map[string]interface{}, defaults map[string]interface{}, options map[string]interface{}, angularOpts map[string]interface{}) { - // Migrate table column - // Based on sharedSingleStatPanelChangedHandler line ~125: options.reduceOptions.fields = `/^${prevPanel.tableColumn}$/` - if tableColumn, ok := angularOpts["tableColumn"].(string); ok && tableColumn != "" { - options["reduceOptions"].(map[string]interface{})["fields"] = "/^" + tableColumn + "$/" - } - - // Migrate unit from format property (matches frontend sharedSingleStatPanelChangedHandler) - if format, ok := angularOpts["format"].(string); ok { - defaults["unit"] = format - } - - // Migrate decimals - if decimals, ok := angularOpts["decimals"]; ok { - defaults["decimals"] = decimals - } - - // Note: Frontend migrateFromAngularSinglestat does migrate nullPointMode to nullValueMode - // but the frontend's getSaveModel() method removes it, so we don't add it here - // if nullPointMode, ok := angularOpts["nullPointMode"]; ok { - // defaults["nullValueMode"] = nullPointMode - // } - - // Migrate null text - if nullText, ok := angularOpts["nullText"].(string); ok { - defaults["noValue"] = nullText - } - - // Migrate value mappings (thresholds should already be migrated) - valueMaps, _ := angularOpts["valueMaps"].([]interface{}) - migrateValueMappings(angularOpts, defaults, valueMaps) - - // Migrate sparkline configuration - // Based on statPanelChangedHandler lines ~20-23: sparkline migration logic - if sparkline, ok := angularOpts["sparkline"].(map[string]interface{}); ok { - if show, ok := sparkline["show"].(bool); ok && show { - options["graphMode"] = "area" - } else { - options["graphMode"] = "none" - } - } else { - // Default to no graph mode if no sparkline configuration - options["graphMode"] = "none" - } - - // Migrate color configuration - // Based on statPanelChangedHandler lines ~25-38: colorBackground and colorValue migration - colorMode := determineColorMode(angularOpts) - options["colorMode"] = colorMode - - // Sparkline color migration only happens when colorMode is "none" - if colorMode == "none" { - migrateSparklineColor(angularOpts, defaults, options) - } - - // Migrate text mode - // Based on statPanelChangedHandler lines ~45-47: valueName === 'name' migration - if valueName, ok := angularOpts["valueName"].(string); ok && valueName == "name" { - options["textMode"] = "name" - } - - if angularOpts["gauge"] != nil && angularOpts["gauge"].(map[string]interface{})["show"] == true { - defaults["min"] = angularOpts["gauge"].(map[string]interface{})["minValue"] - defaults["max"] = angularOpts["gauge"].(map[string]interface{})["maxValue"] - } -} - -// applyCompleteStatPanelDefaults applies the complete stat panel defaults -// This matches the frontend's getPanelOptionsWithDefaults behavior after migration -func applyCompleteStatPanelDefaults(panel map[string]interface{}) { - // Get or create options object - options, exists := panel["options"].(map[string]interface{}) - if !exists { - options = map[string]interface{}{} - panel["options"] = options - } - - defaultOptions := getDefaultStatOptions() - - // Merge defaults with existing options, but don't override existing values - // This matches the frontend's getPanelOptionsWithDefaults behavior - for key, defaultValue := range defaultOptions { - if _, exists := options[key]; !exists { - options[key] = defaultValue - } - } -} - -// applySharedSinglestatMigration applies shared migration logic for all singlestat panels -// Based on sharedSingleStatMigrationHandler in packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts -func applySharedSinglestatMigration(defaults map[string]interface{}) { - // Ensure thresholds have proper structure - if thresholds, ok := defaults["thresholds"].(map[string]interface{}); ok { - if steps, ok := thresholds["steps"].([]interface{}); ok { - // Ensure first threshold is -Infinity (represented as null in JSON) - if len(steps) > 0 { - if firstStep, ok := steps[0].(map[string]interface{}); ok { - if firstStep["value"] == nil { - firstStep["value"] = nil // Use null instead of -math.Inf(1) - } - } - } - } - } - - // Handle percent/percentunit units - // Based on sharedSingleStatMigrationHandler lines ~280-300: percent/percentunit min/max handling - if unit, ok := defaults["unit"].(string); ok { - switch unit { - case "percent": - if defaults["min"] == nil { - defaults["min"] = 0 - } - if defaults["max"] == nil { - defaults["max"] = 100 - } - case "percentunit": - if defaults["min"] == nil { - defaults["min"] = 0 - } - if defaults["max"] == nil { - defaults["max"] = 1 - } - } - } -} - -// Helper functions - -func extractAngularOptions(panel map[string]interface{}) map[string]interface{} { - // Some panels might have angular options directly in the root - // Check for common angular properties - angularProps := []string{ - "valueName", "tableColumn", "format", "decimals", "nullPointMode", "nullText", - "thresholds", "colors", "valueMaps", "gauge", "sparkline", "colorBackground", "colorValue", - } - for _, prop := range angularProps { - if _, exists := panel[prop]; exists { - return panel - } - } - - return map[string]interface{}{} -} - -// getReducerForValueName returns the mapped reducer or empty string for invalid values -func getReducerForValueName(valueName string) string { - reducerMap := map[string]string{ - "min": "min", - "max": "max", - "mean": "mean", - "avg": "mean", // avg maps to mean - "median": "median", - "sum": "sum", - "count": "count", - "first": "firstNotNull", - "last": "lastNotNull", - "name": "lastNotNull", - "current": "lastNotNull", - "total": "sum", - } - - if reducer, ok := reducerMap[valueName]; ok { - return reducer - } - - return "" -} - -func migrateThresholdsAndColors(defaults map[string]interface{}, thresholdsStr string, colors []interface{}) { - // Parse thresholds string (e.g., "10,20,30") - // Based on sharedSingleStatPanelChangedHandler lines ~145-165: Convert thresholds and color values - thresholds := []interface{}{} - thresholdValues := strings.Split(thresholdsStr, ",") - - // Create threshold steps - for i, color := range colors { - step := map[string]interface{}{ - "color": color, - } - - if i == 0 { - // Frontend expects explicit null value for first step, not omitted field - // Use a pointer to ensure the field is present in JSON with null value - var nullValue *float64 - step["value"] = nullValue - } else if i-1 < len(thresholdValues) { - if val, err := strconv.ParseFloat(strings.TrimSpace(thresholdValues[i-1]), 64); err == nil { - step["value"] = val - } - } - - thresholds = append(thresholds, step) - } - - defaults["thresholds"] = map[string]interface{}{ - "mode": "absolute", - "steps": thresholds, - } -} - -func migrateValueMappings(panel map[string]interface{}, defaults map[string]interface{}, valueMappings []interface{}) { - mappings := []interface{}{} - mappingType := panel["mappingType"] - - // Check for inconsistent mapping configuration - // If panel has rangeMaps but mappingType is 1, or vice versa, fix it - hasValueMaps := panel["valueMaps"] != nil && IsArray(panel["valueMaps"]) && len(panel["valueMaps"].([]interface{})) > 0 - hasRangeMaps := panel["rangeMaps"] != nil && IsArray(panel["rangeMaps"]) && len(panel["rangeMaps"].([]interface{})) > 0 - - if hasRangeMaps && mappingType == float64(1) { - mappingType = 2 - } else if hasValueMaps && mappingType == float64(2) { - mappingType = 1 - } else if mappingType == nil { - if hasValueMaps { - mappingType = 1 - } else if hasRangeMaps { - mappingType = 2 - } - } - - switch mappingType { - case 1: - for _, valueMap := range valueMappings { - valueMapping := valueMap.(map[string]interface{}) - upgradedMapping := upgradeOldAngularValueMapping(valueMapping, defaults["thresholds"]) - if upgradedMapping != nil { - mappings = append(mappings, upgradedMapping) - } - } - case 2: - // Handle range mappings - if rangeMaps, ok := panel["rangeMaps"].([]interface{}); ok { - for _, rangeMap := range rangeMaps { - rangeMapping := rangeMap.(map[string]interface{}) - upgradedMapping := upgradeOldAngularValueMapping(rangeMapping, defaults["thresholds"]) - if upgradedMapping != nil { - mappings = append(mappings, upgradedMapping) - } - } - } - } - - defaults["mappings"] = mappings -} - -// upgradeOldAngularValueMapping converts old angular value mappings to new format -// Based on upgradeOldAngularValueMapping in packages/grafana-data/src/utils/valueMappings.ts -func upgradeOldAngularValueMapping(old map[string]interface{}, thresholds interface{}) map[string]interface{} { - valueMaps := map[string]interface{}{ - "type": "value", - "options": map[string]interface{}{}, - } - newMappings := []interface{}{} - - // Use the color we would have picked from thresholds - // Frontend uses old.text to determine color, not old.value - var color interface{} - if text, ok := old["text"].(string); ok { - if numeric, err := parseNumericValue(text); err == nil { - if thresholdsMap, ok := thresholds.(map[string]interface{}); ok { - if steps, ok := thresholdsMap["steps"].([]interface{}); ok { - level := getActiveThreshold(numeric, steps) - if level != nil { - if levelColor, ok := level["color"]; ok { - color = levelColor - } - } - } - } - } - } - - // Determine mapping type - mappingType := old["type"] - if mappingType == nil { - // Try to guess from available properties - if old["value"] != nil { - mappingType = 1 // ValueToText - } else if old["from"] != nil || old["to"] != nil { - mappingType = 2 // RangeToText - } - } - - switch mappingType { - case 1: // ValueToText - if value, ok := old["value"]; ok && value != nil { - if valueStr, ok := value.(string); ok && valueStr == "null" { - newMappings = append(newMappings, map[string]interface{}{ - "type": "special", - "options": map[string]interface{}{ - "match": "null", - "result": map[string]interface{}{"text": old["text"], "color": color}, - }, - }) - } else { - valueMaps["options"].(map[string]interface{})[fmt.Sprintf("%v", value)] = map[string]interface{}{ - "text": old["text"], - "color": color, - } - } - } - case 2: // RangeToText - from := old["from"] - to := old["to"] - if (from != nil && fmt.Sprintf("%v", from) == "null") || (to != nil && fmt.Sprintf("%v", to) == "null") { - newMappings = append(newMappings, map[string]interface{}{ - "type": "special", - "options": map[string]interface{}{ - "match": "null", - "result": map[string]interface{}{"text": old["text"], "color": color}, - }, - }) - } else { - var fromVal, toVal interface{} - if from != nil { - if fromStr, ok := from.(string); ok { - if fromFloat, err := strconv.ParseFloat(fromStr, 64); err == nil { - fromVal = fromFloat - } - } else { - fromVal = from - } - } - if to != nil { - if toStr, ok := to.(string); ok { - if toFloat, err := strconv.ParseFloat(toStr, 64); err == nil { - toVal = toFloat - } - } else { - toVal = to - } - } - - newMappings = append(newMappings, map[string]interface{}{ - "type": "range", - "options": map[string]interface{}{ - "from": fromVal, - "to": toVal, - "result": map[string]interface{}{"text": old["text"], "color": color}, - }, - }) - } - } - - // Add valueMaps if it has options - if len(valueMaps["options"].(map[string]interface{})) > 0 { - newMappings = append([]interface{}{valueMaps}, newMappings...) - } - - if len(newMappings) > 0 { - return newMappings[0].(map[string]interface{}) - } - - return nil -} - -// getActiveThreshold finds the active threshold for a given value -// Based on getActiveThreshold in packages/grafana-data/src/field/thresholds.ts -func getActiveThreshold(value float64, steps []interface{}) map[string]interface{} { - for i := len(steps) - 1; i >= 0; i-- { - if step, ok := steps[i].(map[string]interface{}); ok { - if stepValue, ok := step["value"]; ok { - if stepValue == nil { - // First step with null value (represents -Infinity) - return step - } - if stepFloat, ok := stepValue.(float64); ok && value >= stepFloat { - return step - } - } - } - } - return nil -} - -// parseNumericValue converts various types to float64 for threshold calculations -func parseNumericValue(value interface{}) (float64, error) { - switch v := value.(type) { - case string: - return strconv.ParseFloat(v, 64) - case float64: - return v, nil - case float32: - return float64(v), nil - case int: - return float64(v), nil - case int32: - return float64(v), nil - case int64: - return float64(v), nil - default: - return 0, fmt.Errorf("cannot convert %T to numeric value", value) - } -} - -// createFieldConfigFromDefaults creates the proper fieldConfig structure from defaults -// and removes all legacy properties from the panel -func createFieldConfigFromDefaults(panel map[string]interface{}, defaults map[string]interface{}) { - // Ensure fieldConfig exists - if panel["fieldConfig"] == nil { - panel["fieldConfig"] = map[string]interface{}{ - "defaults": map[string]interface{}{}, - "overrides": []interface{}{}, - } - } - - fieldConfig := panel["fieldConfig"].(map[string]interface{}) - fieldDefaults := fieldConfig["defaults"].(map[string]interface{}) - - // Copy all defaults to fieldConfig.defaults - for key, value := range defaults { - fieldDefaults[key] = value - } - - // Note: Frontend doesn't add these extra fieldConfig defaults - // Color is handled in sparkline migration logic - // nullValueMode and unit are not added by frontend - - // Remove all legacy properties from the panel - legacyProperties := []string{ - "colors", "thresholds", "valueMaps", "grid", "legend", "mappingTypes", "gauge", - "autoMigrateFrom", "colorBackground", "colorValue", "format", "mappingType", - "nullPointMode", "postfix", "postfixFontSize", "prefix", - "prefixFontSize", "rangeMaps", "sparkline", "tableColumn", "valueFontSize", - "valueName", "aliasYAxis", "bars", "dashLength", "dashes", "fill", "fillGradient", - "lineInterpolation", "lineWidth", "pointRadius", "points", "spaceLength", - "stack", "steppedLine", "xAxis", "yAxes", "yAxis", "zIndex", - } - - for _, prop := range legacyProperties { - delete(panel, prop) - } -} - -// cleanupAngularProperties removes old angular properties after migration -// Based on PanelModel.clearPropertiesBeforePluginChange in public/app/features/dashboard/state/PanelModel.ts -// This function removes ALL properties except those in mustKeepProps to match frontend behavior exactly -func cleanupAngularProperties(panel map[string]interface{}) { - // Properties that must be kept (matching frontend mustKeepProps) - mustKeepProps := map[string]bool{ - "id": true, "gridPos": true, "type": true, "title": true, "scopedVars": true, - "repeat": true, "repeatPanelId": true, "repeatDirection": true, "repeatedByRow": true, - "minSpan": true, "collapsed": true, "panels": true, "targets": true, "datasource": true, - "timeFrom": true, "timeShift": true, "hideTimeOverride": true, "description": true, - "links": true, "fullscreen": true, "isEditing": true, "isViewing": true, - "hasRefreshed": true, "events": true, "cacheTimeout": true, "queryCachingTTL": true, - "cachedPluginOptions": true, "transparent": true, "pluginVersion": true, - "fieldConfig": true, "options": true, // These are set by migration - "maxDataPoints": true, "interval": true, // Panel-level properties preserved by frontend - "autoMigrateFrom": true, // Preserve autoMigrateFrom for proper migration logic - } - - // Remove ALL properties except those in mustKeepProps (matching frontend behavior) - for key := range panel { - if !mustKeepProps[key] { - delete(panel, key) - } - } - - // Ensure all targets have refIds (matching frontend ensureQueryIds behavior) - ensureTargetRefIds(panel) -} - -// ensureTargetRefIds assigns refIds to targets that don't have them -// This matches the frontend PanelModel.ensureQueryIds() behavior -func ensureTargetRefIds(panel map[string]interface{}) { - targets, ok := panel["targets"].([]interface{}) - if !ok || len(targets) == 0 { - return - } - - // Find existing refIds - existingRefIds := make(map[string]bool) - for _, targetInterface := range targets { - if target, ok := targetInterface.(map[string]interface{}); ok { - if refId, ok := target["refId"].(string); ok { - existingRefIds[refId] = true - } - } - } - - // Assign refIds to targets that don't have them - letters := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - letterIndex := 0 - - for _, targetInterface := range targets { - if target, ok := targetInterface.(map[string]interface{}); ok { - refId, hasRefId := target["refId"].(string) - if !hasRefId || refId == "" { - // Find next available refId - for letterIndex < len(letters) { - refId := string(letters[letterIndex]) - if !existingRefIds[refId] { - target["refId"] = refId - existingRefIds[refId] = true - break - } - letterIndex++ - } - letterIndex++ - } - } - } -} - // removeDeprecatedVariableProperties removes deprecated properties from variables // Based on DashboardMigrator.ts v28 migration: variable property cleanup func removeDeprecatedVariableProperties(variable map[string]interface{}) { @@ -843,45 +69,3 @@ func removeDeprecatedVariableProperties(variable map[string]interface{}) { } } } - -// determineColorMode determines the color mode based on angular options -func determineColorMode(angularOpts map[string]interface{}) string { - if colorBackground, ok := angularOpts["colorBackground"].(bool); ok && colorBackground { - return "background" - } - - if colorValue, ok := angularOpts["colorValue"].(bool); ok && colorValue { - return "value" - } - - return "none" -} - -// migrateSparklineColor migrates sparkline color configuration when colorMode is "none" -// Based on statPanelChangedHandler lines 31-38 -func migrateSparklineColor(angularOpts map[string]interface{}, defaults map[string]interface{}, options map[string]interface{}) { - sparkline, ok := angularOpts["sparkline"].(map[string]interface{}) - if !ok { - return - } - - show, ok := sparkline["show"].(bool) - if !ok || !show { - return - } - - graphMode, ok := options["graphMode"].(string) - if !ok || graphMode != "area" { - return - } - - lineColor, ok := sparkline["lineColor"].(string) - if !ok { - return - } - - defaults["color"] = map[string]interface{}{ - "mode": "fixed", - "fixedColor": lineColor, - } -} diff --git a/apps/dashboard/pkg/migration/schemaversion/v28_test.go b/apps/dashboard/pkg/migration/schemaversion/v28_test.go index 68fc1c5fedd..b597d913f98 100644 --- a/apps/dashboard/pkg/migration/schemaversion/v28_test.go +++ b/apps/dashboard/pkg/migration/schemaversion/v28_test.go @@ -5,101 +5,186 @@ import ( "testing" ) -func TestV28SinglestatMigration(t *testing.T) { - tests := []struct { - name string - input map[string]interface{} - expected map[string]interface{} - description string - }{ - { - name: "migrate_range_maps_to_field_config_mappings", - input: map[string]interface{}{ - "type": "singlestat", - "rangeMaps": []interface{}{ - map[string]interface{}{ - "from": "null", - "to": "N/A", - }, - }, - "mappingType": 1, // Inconsistent - should be 2 for rangeMaps - }, - expected: map[string]interface{}{ - "type": "stat", - "fieldConfig": map[string]interface{}{ - "defaults": map[string]interface{}{ - "mappings": []interface{}{ - map[string]interface{}{ - "options": map[string]interface{}{ - "match": "null", - "result": map[string]interface{}{ - "text": "N/A", - }, - }, - "type": "special", - }, - }, - }, - }, - }, - description: "RangeMaps should migrate to fieldConfig.mappings, and inconsistent mappingType should be fixed to 2 (RangeToText)", - }, - { - name: "migrate_sparkline_color_when_color_mode_none", - input: map[string]interface{}{ - "type": "singlestat", - "colorMode": "None", - "sparkline": map[string]interface{}{ - "lineColor": "rgb(31, 120, 193)", - }, - }, - expected: map[string]interface{}{ - "type": "stat", - "fieldConfig": map[string]interface{}{ - "defaults": map[string]interface{}{ - "color": map[string]interface{}{ - "mode": "fixed", - "fixedColor": "rgb(31, 120, 193)", - }, - }, - }, - }, - description: "Sparkline lineColor should migrate to fieldConfig.defaults.color only when colorMode is None", - }, - } +type migrationTestCase struct { + name string + input map[string]interface{} + expected map[string]interface{} +} +func runMigrationTests(t *testing.T, tests []migrationTestCase, migrationFunc func(context.Context, map[string]interface{}) error) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - dashboard := map[string]interface{}{ - "schemaVersion": 27, - "panels": []interface{}{tt.input}, + // Create a copy of the input + dashboard := make(map[string]interface{}) + for k, v := range tt.input { + dashboard[k] = v } - err := V28(context.Background(), dashboard) + err := migrationFunc(context.Background(), dashboard) if err != nil { - t.Fatalf("V28 migration failed: %v", err) + t.Fatalf("Migration failed: %v", err) } - if dashboard["schemaVersion"] != 28 { - t.Errorf("Expected schemaVersion to be 28, got %v", dashboard["schemaVersion"]) + // Verify the result matches expected + if !deepEqual(dashboard, tt.expected) { + t.Errorf("Migration result doesn't match expected.\nExpected: %+v\nGot: %+v", tt.expected, dashboard) } - - panels, ok := dashboard["panels"].([]interface{}) - if !ok || len(panels) == 0 { - t.Fatalf("Expected panels array with at least one panel") - } - - panel, ok := panels[0].(map[string]interface{}) - if !ok { - t.Fatalf("Expected panel to be a map") - } - - // Verify panel type was changed to stat - if panel["type"] != "stat" { - t.Errorf("Expected panel type to be 'stat', got %v", panel["type"]) - } - - t.Logf("✓ %s: %s", tt.name, tt.description) }) } } + +func deepEqual(a, b interface{}) bool { + // Simple deep comparison for test purposes + // This is a simplified version - in production you'd use reflect.DeepEqual or similar + switch aVal := a.(type) { + case map[string]interface{}: + bVal, ok := b.(map[string]interface{}) + if !ok || len(aVal) != len(bVal) { + return false + } + for k, v := range aVal { + if !deepEqual(v, bVal[k]) { + return false + } + } + return true + case []interface{}: + bVal, ok := b.([]interface{}) + if !ok || len(aVal) != len(bVal) { + return false + } + for i, v := range aVal { + if !deepEqual(v, bVal[i]) { + return false + } + } + return true + default: + return a == b + } +} + +func TestV28(t *testing.T) { + tests := []migrationTestCase{ + { + name: "v28 removes deprecated variable properties", + input: map[string]interface{}{ + "title": "V28 Variable Properties Migration Test Dashboard", + "schemaVersion": 27, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "name": "var1", + "tags": []interface{}{"tag1", "tag2"}, + "tagsQuery": "query_string", + "tagValuesQuery": "values_query", + "useTags": true, + "type": "query", + }, + map[string]interface{}{ + "name": "var2", + "tags": []interface{}{}, + "tagsQuery": "", // Empty string should not be removed + "tagValuesQuery": "", // Empty string should not be removed + "useTags": false, // False should not be removed + "type": "custom", + }, + }, + }, + "panels": []interface{}{ + map[string]interface{}{ + "type": "singlestat", + "title": "Singlestat Panel (unchanged by v28)", + "id": 1, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V28 Variable Properties Migration Test Dashboard", + "schemaVersion": 28, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "name": "var1", + "type": "query", + // tags, tagsQuery, tagValuesQuery, useTags should be removed + }, + map[string]interface{}{ + "name": "var2", + "tagsQuery": "", // Empty string preserved + "tagValuesQuery": "", // Empty string preserved + "useTags": false, // False preserved + "type": "custom", + // only tags should be removed + }, + }, + }, + "panels": []interface{}{ + map[string]interface{}{ + "type": "singlestat", + "title": "Singlestat Panel (unchanged by v28)", + "id": 1, + }, + }, + }, + }, + { + name: "v28 handles dashboard without templating", + input: map[string]interface{}{ + "title": "Dashboard without templating", + "schemaVersion": 27, + "panels": []interface{}{ + map[string]interface{}{ + "type": "singlestat", + "title": "Singlestat Panel", + "id": 1, + }, + }, + }, + expected: map[string]interface{}{ + "title": "Dashboard without templating", + "schemaVersion": 28, + "panels": []interface{}{ + map[string]interface{}{ + "type": "singlestat", + "title": "Singlestat Panel", + "id": 1, + }, + }, + }, + }, + { + name: "v28 handles empty templating list", + input: map[string]interface{}{ + "title": "Dashboard with empty templating", + "schemaVersion": 27, + "templating": map[string]interface{}{ + "list": []interface{}{}, + }, + "panels": []interface{}{ + map[string]interface{}{ + "type": "singlestat", + "title": "Singlestat Panel", + "id": 1, + }, + }, + }, + expected: map[string]interface{}{ + "title": "Dashboard with empty templating", + "schemaVersion": 28, + "templating": map[string]interface{}{ + "list": []interface{}{}, + }, + "panels": []interface{}{ + map[string]interface{}{ + "type": "singlestat", + "title": "Singlestat Panel", + "id": 1, + }, + }, + }, + }, + } + + runMigrationTests(t, tests, V28) +} diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-mssql/mssql_fakedata.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-mssql/mssql_fakedata.v42.json index 9d091b0a35d..45c41bc2847 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-mssql/mssql_fakedata.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-mssql/mssql_fakedata.v42.json @@ -327,60 +327,12 @@ } }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-mssql" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 10, "w": 24, @@ -388,11 +340,35 @@ "y": 18 }, "id": 4, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -405,14 +381,7 @@ } ], "title": "Values", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" } ], diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-mssql/mssql_unittest.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-mssql/mssql_unittest.v42.json index 9f651c9bf02..36dbc69d1cc 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-mssql/mssql_unittest.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-mssql/mssql_unittest.v42.json @@ -77,40 +77,12 @@ "links": [], "panels": [ { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-mssql-ds-tests" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, + "fontSize": "100%", "gridPos": { "h": 4, "w": 24, @@ -118,11 +90,28 @@ "y": 0 }, "id": 2, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "string", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -135,71 +124,16 @@ } ], "title": "Data types", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-mssql-ds-tests" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "time" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 3, "w": 6, @@ -207,11 +141,35 @@ "y": 4 }, "id": 32, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -225,71 +183,16 @@ } ], "title": "cast(null as bigint) as time", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-mssql-ds-tests" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "time" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 3, "w": 6, @@ -297,11 +200,35 @@ "y": 4 }, "id": 33, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -315,71 +242,16 @@ } ], "title": "cast(null as datetime) as time", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-mssql-ds-tests" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "time" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 3, "w": 6, @@ -387,11 +259,35 @@ "y": 4 }, "id": 34, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -405,71 +301,16 @@ } ], "title": "GETDATE() as time", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-mssql-ds-tests" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "time" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 3, "w": 6, @@ -477,11 +318,35 @@ "y": 4 }, "id": 35, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -495,14 +360,7 @@ } ], "title": "GETUTCDATE() as time", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" }, { diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-mysql/mysql_fakedata.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-mysql/mysql_fakedata.v42.json index e05dc3f21f7..e946b4f7c53 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-mysql/mysql_fakedata.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-mysql/mysql_fakedata.v42.json @@ -329,60 +329,12 @@ } }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-mysql" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 9, "w": 24, @@ -390,11 +342,36 @@ "y": 18 }, "id": 6, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "link": false, + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -408,14 +385,7 @@ } ], "title": "Values", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" } ], diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-mysql/mysql_unittest.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-mysql/mysql_unittest.v42.json index d528c6d6198..02768fa9b68 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-mysql/mysql_unittest.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-mysql/mysql_unittest.v42.json @@ -77,40 +77,12 @@ "links": [], "panels": [ { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-mysql-ds-tests" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, + "fontSize": "100%", "gridPos": { "h": 4, "w": 24, @@ -118,11 +90,28 @@ "y": 0 }, "id": 2, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "string", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -135,71 +124,16 @@ } ], "title": "Data types", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-mysql-ds-tests" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "time_sec" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 3, "w": 6, @@ -207,11 +141,35 @@ "y": 4 }, "id": 32, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time_sec", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -225,71 +183,16 @@ } ], "title": "cast(null as unsigned integer) as time", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-mysql-ds-tests" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "time_sec" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 3, "w": 6, @@ -297,11 +200,35 @@ "y": 4 }, "id": 33, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time_sec", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -315,71 +242,16 @@ } ], "title": "cast(null as datetime) as time", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-mysql-ds-tests" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "time_sec" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 3, "w": 6, @@ -387,11 +259,35 @@ "y": 4 }, "id": 34, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time_sec", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -405,71 +301,16 @@ } ], "title": "cast()NOW() as datetime) as time", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-mysql-ds-tests" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "time" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 3, "w": 6, @@ -477,11 +318,35 @@ "y": 4 }, "id": 35, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -495,14 +360,7 @@ } ], "title": "NOW() as time", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" }, { diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-postgres/postgres_fakedata.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-postgres/postgres_fakedata.v42.json index 8d8faf154cc..d30bfa70e7e 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-postgres/postgres_fakedata.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-postgres/postgres_fakedata.v42.json @@ -371,60 +371,12 @@ } }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-postgres" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 9, "w": 24, @@ -432,11 +384,36 @@ "y": 18 }, "id": 6, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "link": false, + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -450,14 +427,7 @@ } ], "title": "Values", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" } ], diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-postgres/postgres_unittest.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-postgres/postgres_unittest.v42.json index a86b8d4c1be..3bdbf26db6a 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-postgres/postgres_unittest.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-postgres/postgres_unittest.v42.json @@ -77,40 +77,12 @@ "links": [], "panels": [ { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-postgres-ds-tests" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, + "fontSize": "100%", "gridPos": { "h": 4, "w": 24, @@ -118,11 +90,28 @@ "y": 0 }, "id": 2, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 1, + "desc": false }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "string", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -135,71 +124,16 @@ } ], "title": "Data types", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-postgres-ds-tests" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "time" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 3, "w": 6, @@ -207,11 +141,35 @@ "y": 4 }, "id": 32, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -225,71 +183,16 @@ } ], "title": "cast(null as bigint) as time", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-postgres-ds-tests" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "time" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 3, "w": 6, @@ -297,11 +200,35 @@ "y": 4 }, "id": 33, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -315,71 +242,16 @@ } ], "title": "cast(null as datetime) as time", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-postgres-ds-tests" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "time" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 3, "w": 6, @@ -387,11 +259,35 @@ "y": 4 }, "id": 34, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -405,71 +301,16 @@ } ], "title": "localtimestamp as time", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "uid": "gdev-postgres-ds-tests" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "time" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 3, "w": 6, @@ -477,11 +318,35 @@ "y": 4 }, "id": 35, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "", @@ -495,14 +360,7 @@ } ], "title": "NOW() as time", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" }, { diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/feature-templating/testdata-nested-variables-drilldown.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/feature-templating/testdata-nested-variables-drilldown.v42.json index d86c55cc494..c1a9689ddd8 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/feature-templating/testdata-nested-variables-drilldown.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/feature-templating/testdata-nested-variables-drilldown.v42.json @@ -49,44 +49,26 @@ "type": "text" }, { + "autoMigrateFrom": "singlestat", + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], "datasource": { "apiVersion": "v1", "type": "grafana-testdata-datasource", "uid": "testdata-type-uid" }, - "fieldConfig": { - "defaults": { - "color": { - "fixedColor": "rgb(31, 120, 193)", - "mode": "fixed" - }, - "mappings": [ - { - "options": { - "match": "null", - "result": { - "text": "N/A" - } - }, - "type": "special" - } - ], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true }, "gridPos": { "h": 6, @@ -102,25 +84,37 @@ "url": "d/-Y-tnEDWk/dashboard-tests-nested-template-variables?orgId=1\u0026${__all_variables}\u0026${__url_time_range}" } ], - "maxDataPoints": 100, - "options": { - "colorMode": "none", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "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 }, - "pluginVersion": "12.1.0", + "tableColumn": "", "targets": [ { "datasource": { @@ -132,8 +126,18 @@ "scenarioId": "random_walk" } ], + "thresholds": "", "title": "Panel drilldown link test", - "type": "stat" + "type": "stat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" }, { "aliasColors": {}, diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/feature-templating/testdata-nested-variables.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/feature-templating/testdata-nested-variables.v42.json index a7e8cab830f..8fce11b3b1a 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/feature-templating/testdata-nested-variables.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/feature-templating/testdata-nested-variables.v42.json @@ -55,43 +55,31 @@ "type": "text" }, { + "autoMigrateFrom": "singlestat", + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], "datasource": { "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { - "color": { - "fixedColor": "rgb(31, 120, 193)", - "mode": "fixed" - }, - "mappings": [ - { - "options": { - "match": "null", - "result": { - "text": "N/A" - } - }, - "type": "special" - } - ], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" + "custom": {} }, "overrides": [] }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, "gridPos": { "h": 9, "w": 4, @@ -106,25 +94,28 @@ "url": "d/O6GmNPvWk/dashboard-tests-nested-template-variables-drilldown?orgId=1\u0026${__all_variables}\u0026${__url_time_range}" } ], + "mappingType": 1, + "mappingTypes": [], "maxDataPoints": 100, - "options": { - "colorMode": "none", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "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 }, - "pluginVersion": "12.1.0", + "tableColumn": "", "targets": [ { "datasource": { @@ -134,8 +125,18 @@ "scenarioId": "random_walk" } ], + "thresholds": "", "title": "Panel drilldown link test", - "type": "stat" + "type": "stat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" }, { "datasource": { diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-common/lazy_loading.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-common/lazy_loading.v42.json index da8363866ae..0079708be32 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-common/lazy_loading.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-common/lazy_loading.v42.json @@ -191,38 +191,24 @@ "type": "bargauge" }, { + "autoMigrateFrom": "singlestat", + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "#73BF69", + "#d44a3a" + ], "datasource": { "type": "grafana-testdata-datasource" }, - "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": [] + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true }, "gridPos": { "h": 4, @@ -231,25 +217,38 @@ "y": 7 }, "id": 20, - "maxDataPoints": 100, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "pluginVersion": "6.2.0-pre", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "p99", + "prefixFontSize": "50%", + "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 }, - "pluginVersion": "12.1.0", + "tableColumn": "", "targets": [ { "datasource": { @@ -259,41 +258,37 @@ "scenarioId": "random_walk" } ], - "type": "stat" + "thresholds": "", + "type": "stat", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" }, { + "autoMigrateFrom": "singlestat", + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "#73BF69", + "#d44a3a" + ], "datasource": { "type": "grafana-testdata-datasource" }, - "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": [] + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true }, "gridPos": { "h": 4, @@ -302,25 +297,38 @@ "y": 7 }, "id": 23, - "maxDataPoints": 100, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true + { + "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 }, - "pluginVersion": "12.1.0", + "tableColumn": "", "targets": [ { "datasource": { @@ -330,41 +338,37 @@ "scenarioId": "random_walk" } ], - "type": "stat" + "thresholds": "", + "type": "stat", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" }, { + "autoMigrateFrom": "singlestat", + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "#73BF69", + "#d44a3a" + ], "datasource": { "type": "grafana-testdata-datasource" }, - "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": [] + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true }, "gridPos": { "h": 4, @@ -373,25 +377,38 @@ "y": 7 }, "id": 24, - "maxDataPoints": 100, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "pluginVersion": "6.2.0-pre", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "p90", + "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 }, - "pluginVersion": "12.1.0", + "tableColumn": "", "targets": [ { "datasource": { @@ -401,41 +418,37 @@ "scenarioId": "random_walk" } ], - "type": "stat" + "thresholds": "", + "type": "stat", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" }, { + "autoMigrateFrom": "singlestat", + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "#73BF69", + "#d44a3a" + ], "datasource": { "type": "grafana-testdata-datasource" }, - "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": [] + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true }, "gridPos": { "h": 4, @@ -444,25 +457,38 @@ "y": 7 }, "id": 45, - "maxDataPoints": 100, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "pluginVersion": "6.2.0-pre", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "p90", + "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 }, - "pluginVersion": "12.1.0", + "tableColumn": "", "targets": [ { "datasource": { @@ -472,7 +498,17 @@ "scenarioId": "random_walk" } ], - "type": "stat" + "thresholds": "", + "type": "stat", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" }, { "aliasColors": { diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-common/panels_without_title.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-common/panels_without_title.v42.json index 68e5cbefd7c..d7fa2960e45 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-common/panels_without_title.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-common/panels_without_title.v42.json @@ -20,45 +20,27 @@ "links": [], "panels": [ { + "autoMigrateFrom": "singlestat", + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "#5794F2", + "#d44a3a" + ], "datasource": { "apiVersion": "v1", "type": "grafana-testdata-datasource", "uid": "testdata-type-uid" }, "description": "asdasdas", - "fieldConfig": { - "defaults": { - "color": { - "fixedColor": "rgb(31, 120, 193)", - "mode": "fixed" - }, - "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": [] + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true }, "gridPos": { "h": 4, @@ -67,25 +49,37 @@ "y": 0 }, "id": 8, - "maxDataPoints": 100, - "options": { - "colorMode": "none", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "100%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true }, - "pluginVersion": "12.1.0", + "tableColumn": "", "targets": [ { "datasource": { @@ -97,50 +91,42 @@ "scenarioId": "random_walk" } ], + "thresholds": "", "timeShift": "2h", "title": "Title", - "type": "stat" + "type": "stat", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" }, { + "autoMigrateFrom": "singlestat", + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "#5794F2", + "#d44a3a" + ], "datasource": { "apiVersion": "v1", "type": "grafana-testdata-datasource", "uid": "testdata-type-uid" }, "description": "asdasdas", - "fieldConfig": { - "defaults": { - "color": { - "fixedColor": "rgb(31, 120, 193)", - "mode": "fixed" - }, - "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": [] + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true }, "gridPos": { "h": 4, @@ -149,25 +135,37 @@ "y": 0 }, "id": 2, - "maxDataPoints": 100, - "options": { - "colorMode": "none", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "100%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true }, - "pluginVersion": "12.1.0", + "tableColumn": "", "targets": [ { "datasource": { @@ -179,49 +177,41 @@ "scenarioId": "random_walk" } ], + "thresholds": "", "timeShift": "2h", - "type": "stat" + "type": "stat", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" }, { + "autoMigrateFrom": "singlestat", + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "#5794F2", + "#d44a3a" + ], "datasource": { "apiVersion": "v1", "type": "grafana-testdata-datasource", "uid": "testdata-type-uid" }, "description": "asdasdas", - "fieldConfig": { - "defaults": { - "color": { - "fixedColor": "rgb(31, 120, 193)", - "mode": "fixed" - }, - "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": [] + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true }, "gridPos": { "h": 4, @@ -230,25 +220,37 @@ "y": 4 }, "id": 4, - "maxDataPoints": 100, - "options": { - "colorMode": "none", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "100%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true }, - "pluginVersion": "12.1.0", + "tableColumn": "", "targets": [ { "datasource": { @@ -260,9 +262,19 @@ "scenarioId": "random_walk" } ], + "thresholds": "", "timeShift": "2h", "title": "Panel Title", - "type": "stat" + "type": "stat", + "valueFontSize": "120%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" }, { "datasource": { @@ -404,43 +416,27 @@ "type": "text" }, { + "autoMigrateFrom": "singlestat", + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], "datasource": { "apiVersion": "v1", "type": "grafana-testdata-datasource", "uid": "testdata-type-uid" }, "description": "asdasdas", - "fieldConfig": { - "defaults": { - "mappings": [ - { - "options": { - "match": "null", - "result": { - "text": "N/A" - } - }, - "type": "special" - } - ], - "max": 100, - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true }, "gridPos": { "h": 4, @@ -449,25 +445,38 @@ "y": 8 }, "id": 10, - "maxDataPoints": 100, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "pluginVersion": "6.2.0-pre", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false }, - "pluginVersion": "12.1.0", + "tableColumn": "", "targets": [ { "datasource": { @@ -479,8 +488,18 @@ "scenarioId": "random_walk" } ], + "thresholds": "", "timeShift": "2h", - "type": "stat" + "type": "stat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" }, { "datasource": { diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-table/table_tests.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-table/table_tests.v42.json index bdac0888407..f2f7abc07d6 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-table/table_tests.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-table/table_tests.v42.json @@ -20,150 +20,12 @@ "links": [], "panels": [ { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "type": "grafana-testdata-datasource" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "ColorCell" - }, - "properties": [ - { - "id": "unit", - "value": "currencyUSD" - }, - { - "id": "decimals", - "value": 2 - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-background" - } - }, - { - "id": "custom.align" - }, - { - "id": "thresholds", - "value": { - "mode": "absolute", - "steps": [ - { - "color": "rgba(245, 54, 54, 0.9)", - "value": null - }, - { - "color": "rgba(237, 129, 40, 0.89)", - "value": 5 - }, - { - "color": "rgba(50, 172, 45, 0.97)", - "value": 10 - } - ] - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "ColorValue" - }, - "properties": [ - { - "id": "unit", - "value": "Bps" - }, - { - "id": "decimals", - "value": 2 - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-text" - } - }, - { - "id": "custom.align" - }, - { - "id": "thresholds", - "value": { - "mode": "absolute", - "steps": [ - { - "color": "rgba(245, 54, 54, 0.9)", - "value": null - }, - { - "color": "rgba(237, 129, 40, 0.89)", - "value": 5 - }, - { - "color": "rgba(50, 172, 45, 0.97)", - "value": 10 - } - ] - } - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 11, "w": 12, @@ -171,11 +33,76 @@ "y": 0 }, "id": 3, - "options": { - "cellHeight": "sm", - "showHeader": true + "pageSize": 10, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "align": "auto", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "server1", @@ -200,161 +127,29 @@ } ], "title": "Time series to rows (2 pages)", - "transformations": [ - { - "id": "seriesToRows", - "options": { - "reducers": [] - } - } - ], + "transform": "timeseries_to_rows", "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [ + { + "text": "Avg", + "value": "avg" + }, + { + "text": "Max", + "value": "max" + }, + { + "text": "Current", + "value": "current" + } + ], "datasource": { "type": "grafana-testdata-datasource" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "ColorCell" - }, - "properties": [ - { - "id": "unit", - "value": "currencyUSD" - }, - { - "id": "decimals", - "value": 2 - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-background" - } - }, - { - "id": "custom.align" - }, - { - "id": "thresholds", - "value": { - "mode": "absolute", - "steps": [ - { - "color": "rgba(245, 54, 54, 0.9)", - "value": null - }, - { - "color": "rgba(237, 129, 40, 0.89)", - "value": 5 - }, - { - "color": "rgba(50, 172, 45, 0.97)", - "value": 10 - } - ] - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "ColorValue" - }, - "properties": [ - { - "id": "unit", - "value": "Bps" - }, - { - "id": "decimals", - "value": 2 - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-text" - } - }, - { - "id": "custom.align" - }, - { - "id": "thresholds", - "value": { - "mode": "absolute", - "steps": [ - { - "color": "rgba(245, 54, 54, 0.9)", - "value": null - }, - { - "color": "rgba(237, 129, 40, 0.89)", - "value": 5 - }, - { - "color": "rgba(50, 172, 45, 0.97)", - "value": 10 - } - ] - } - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 11, "w": 12, @@ -362,11 +157,76 @@ "y": 0 }, "id": 4, - "options": { - "cellHeight": "sm", - "showHeader": true + "pageSize": 10, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "align": "auto", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "server1", @@ -391,121 +251,16 @@ } ], "title": "Time series aggregations", - "transformations": [ - { - "id": "reduce", - "options": { - "includeTimeField": false, - "reducers": [ - "mean", - "max", - "lastNotNull" - ] - } - } - ], + "transform": "timeseries_aggregations", "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "type": "grafana-testdata-datasource" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - }, - { - "matcher": { - "id": "byRegexp", - "options": "/Color/" - }, - "properties": [ - { - "id": "unit", - "value": "currencyUSD" - }, - { - "id": "decimals", - "value": 2 - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-background" - } - }, - { - "id": "custom.align" - }, - { - "id": "thresholds", - "value": { - "mode": "absolute", - "steps": [ - { - "color": "rgba(245, 54, 54, 0.9)", - "value": null - }, - { - "color": "rgba(237, 129, 40, 0.89)", - "value": 5 - }, - { - "color": "rgba(50, 172, 45, 0.97)", - "value": 10 - } - ] - } - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 7, "w": 24, @@ -513,11 +268,55 @@ "y": 11 }, "id": 5, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colorMode": "row", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "/Color/", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "ColorValue", @@ -533,161 +332,16 @@ } ], "title": "color row by threshold", - "transformations": [ - { - "id": "seriesToColumns", - "options": { - "reducers": [] - } - } - ], + "transform": "timeseries_to_columns", "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "type": "grafana-testdata-datasource" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "ColorCell" - }, - "properties": [ - { - "id": "unit", - "value": "currencyUSD" - }, - { - "id": "decimals", - "value": 2 - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-background" - } - }, - { - "id": "custom.align" - }, - { - "id": "thresholds", - "value": { - "mode": "absolute", - "steps": [ - { - "color": "rgba(245, 54, 54, 0.9)", - "value": null - }, - { - "color": "rgba(237, 129, 40, 0.89)", - "value": 5 - }, - { - "color": "rgba(50, 172, 45, 0.97)", - "value": 10 - } - ] - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "ColorValue" - }, - "properties": [ - { - "id": "unit", - "value": "Bps" - }, - { - "id": "decimals", - "value": 2 - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-text" - } - }, - { - "id": "custom.align" - }, - { - "id": "thresholds", - "value": { - "mode": "absolute", - "steps": [ - { - "color": "rgba(245, 54, 54, 0.9)", - "value": null - }, - { - "color": "rgba(237, 129, 40, 0.89)", - "value": 5 - }, - { - "color": "rgba(50, 172, 45, 0.97)", - "value": 10 - } - ] - } - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 8, "w": 24, @@ -695,11 +349,75 @@ "y": 18 }, "id": 2, - "options": { - "cellHeight": "sm", - "showHeader": true + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "align": "auto", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "ColorValue", @@ -724,181 +442,16 @@ } ], "title": "Column style thresholds \u0026 units", - "transformations": [ - { - "id": "seriesToColumns", - "options": { - "reducers": [] - } - } - ], + "transform": "timeseries_to_columns", "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [], "datasource": { "type": "grafana-testdata-datasource" }, - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 2, - "displayName": "", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "displayName", - "value": "Time" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - }, - { - "id": "custom.align" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "ColorCell" - }, - "properties": [ - { - "id": "unit", - "value": "currencyUSD" - }, - { - "id": "decimals", - "value": 2 - }, - { - "id": "links", - "value": [ - { - "targetBlank": true, - "title": "", - "url": "http://www.grafana.com" - } - ] - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-background" - } - }, - { - "id": "custom.align" - }, - { - "id": "thresholds", - "value": { - "mode": "absolute", - "steps": [ - { - "color": "rgba(245, 54, 54, 0.5)", - "value": null - }, - { - "color": "rgba(237, 129, 40, 0.5)", - "value": 5 - }, - { - "color": "rgba(50, 172, 45, 0.5)", - "value": 10 - } - ] - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "ColorValue" - }, - "properties": [ - { - "id": "unit", - "value": "Bps" - }, - { - "id": "decimals", - "value": 2 - }, - { - "id": "links", - "value": [ - { - "targetBlank": false, - "title": "", - "url": "http://www.grafana.com" - } - ] - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-text" - } - }, - { - "id": "custom.align" - }, - { - "id": "thresholds", - "value": { - "mode": "absolute", - "steps": [ - { - "color": "rgba(245, 54, 54, 0.5)", - "value": null - }, - { - "color": "rgba(237, 129, 40, 0.5)", - "value": 5 - }, - { - "color": "rgba(50, 172, 45, 0.5)", - "value": 10 - } - ] - } - } - ] - } - ] - }, + "fontSize": "100%", "gridPos": { "h": 10, "w": 24, @@ -906,11 +459,82 @@ "y": 26 }, "id": 6, - "options": { - "cellHeight": "sm", - "showHeader": true + "pageSize": 20, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true }, - "pluginVersion": "12.1.0", + "styles": [ + { + "alias": "Time", + "align": "auto", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "align": "auto", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.5)", + "rgba(237, 129, 40, 0.5)", + "rgba(50, 172, 45, 0.5)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "link": true, + "linkTargetBlank": true, + "linkTooltip": "", + "linkUrl": "http://www.grafana.com", + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "align": "auto", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.5)", + "rgba(237, 129, 40, 0.5)", + "rgba(50, 172, 45, 0.5)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "link": true, + "linkUrl": "http://www.grafana.com", + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "align": "auto", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], "targets": [ { "alias": "ColorValue", @@ -935,14 +559,7 @@ } ], "title": "Column style thresholds and links", - "transformations": [ - { - "id": "seriesToColumns", - "options": { - "reducers": [] - } - } - ], + "transform": "timeseries_to_columns", "type": "table" } ], diff --git a/apps/dashboard/pkg/migration/testdata/input/v13.graph_thresholds.json b/apps/dashboard/pkg/migration/testdata/input/v13.graph_thresholds.json new file mode 100644 index 00000000000..27a83b9a232 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/input/v13.graph_thresholds.json @@ -0,0 +1,62 @@ +{ + "title": "V13 Graph Thresholds Migration Test", + "schemaVersion": 12, + "panels": [ + { + "type": "graph", + "id": 1, + "title": "Graph with Line Thresholds", + "grid": { + "threshold1": 200, + "threshold2": 400, + "threshold1Color": "yellow", + "threshold2Color": "red", + "thresholdLine": true + } + }, + { + "type": "graph", + "id": 2, + "title": "Graph with Fill Thresholds", + "grid": { + "threshold1": 100, + "threshold2": 300, + "threshold1Color": "green", + "threshold2Color": "blue", + "thresholdLine": false + } + }, + { + "type": "graph", + "id": 3, + "title": "Graph with Single Threshold", + "grid": { + "threshold1": 150, + "threshold1Color": "orange", + "thresholdLine": true + } + }, + { + "type": "graph", + "id": 4, + "title": "Graph with Existing Thresholds", + "thresholds": [ + { + "value": 50, + "color": "purple", + "colorMode": "custom" + } + ], + "grid": { + "threshold1": 200, + "threshold1Color": "yellow", + "thresholdLine": false + } + }, + { + "type": "singlestat", + "id": 5, + "title": "Non-Graph Panel" + } + ] +} diff --git a/apps/dashboard/pkg/migration/testdata/input/v13.minimal_graph_config.json b/apps/dashboard/pkg/migration/testdata/input/v13.minimal_graph_config.json new file mode 100644 index 00000000000..9911e340073 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/input/v13.minimal_graph_config.json @@ -0,0 +1,34 @@ +{ + "title": "Dashboard with minimal graph panel settings", + "tags": [], + "timezone": "browser", + "editable": true, + "panels": [ + { + "id": 4, + "type": "graph", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "editorMode": "builder", + "expr": "{\"a.utf8.metric 🤘\", job=\"prometheus-utf8\"}", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ] + } + ], + "schemaVersion": 12, + "time": { + "from": "now-6h", + "to": "now" + }, + "templating": { + "list": [] + } +} diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v13.graph_thresholds.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v13.graph_thresholds.v42.json new file mode 100644 index 00000000000..c799c1cb6b8 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v13.graph_thresholds.v42.json @@ -0,0 +1,174 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "grid": { + "threshold1": 200, + "threshold1Color": "yellow", + "threshold2": 400, + "threshold2Color": "red", + "thresholdLine": true + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Graph with Line Thresholds", + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "grid": { + "threshold1": 100, + "threshold1Color": "green", + "threshold2": 300, + "threshold2Color": "blue", + "thresholdLine": false + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Graph with Fill Thresholds", + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "grid": { + "threshold1": 150, + "threshold1Color": "orange", + "thresholdLine": true + }, + "id": 3, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Graph with Single Threshold", + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "grid": { + "threshold1": 200, + "threshold1Color": "yellow", + "thresholdLine": false + }, + "id": 4, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "thresholds": [ + { + "color": "purple", + "colorMode": "custom", + "value": 50 + } + ], + "title": "Graph with Existing Thresholds", + "type": "timeseries" + }, + { + "autoMigrateFrom": "singlestat", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 5, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Non-Graph Panel", + "type": "stat" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V13 Graph Thresholds Migration Test", + "weekStart": "" +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v13.minimal_graph_config.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v13.minimal_graph_config.v42.json new file mode 100644 index 00000000000..e40a580b6ed --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v13.minimal_graph_config.v42.json @@ -0,0 +1,61 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "graph", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "id": 4, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "editorMode": "builder", + "expr": "{\"a.utf8.metric 🤘\", job=\"prometheus-utf8\"}", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Dashboard with minimal graph panel settings", + "weekStart": "" +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v15.no-op-migration.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v15.no-op-migration.v42.json index 05bac4cb246..1cda544d3fd 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v15.no-op-migration.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v15.no-op-migration.v42.json @@ -54,59 +54,13 @@ ] }, { + "autoMigrateFrom": "singlestat", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, - "fieldConfig": { - "defaults": { - "mappings": [ - { - "options": { - "match": "null", - "result": { - "text": "N/A" - } - }, - "type": "special" - } - ], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, "id": 2, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", "targets": [ { "datasource": { @@ -118,7 +72,14 @@ } ], "title": "Memory Usage", - "type": "stat" + "type": "stat", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ] } ], "refresh": "", diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v16.grid_layout_upgrade.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v16.grid_layout_upgrade.v42.json index 31cb9895cf4..e9d3b12001d 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v16.grid_layout_upgrade.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v16.grid_layout_upgrade.v42.json @@ -75,30 +75,12 @@ "type": "timeseries" }, { + "autoMigrateFrom": "singlestat", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, "gridPos": { "h": 7, "w": 12, @@ -106,24 +88,6 @@ "y": 1 }, "id": 2, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", "targets": [ { "datasource": { diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v17.minspan_to_maxperrow.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v17.minspan_to_maxperrow.v42.json index f52bfb99cf5..9b0ddbb85df 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v17.minspan_to_maxperrow.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v17.minspan_to_maxperrow.v42.json @@ -77,30 +77,12 @@ "type": "timeseries" }, { + "autoMigrateFrom": "singlestat", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, "gridPos": { "h": 4, "w": 6, @@ -108,24 +90,7 @@ "y": 8 }, "id": 2, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", + "maxPerRow": 6, "targets": [ { "datasource": { diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v19.panel_links.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v19.panel_links.v42.json index d92b346e5a6..51072542d22 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v19.panel_links.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v19.panel_links.v42.json @@ -153,49 +153,13 @@ "type": "gauge" }, { + "autoMigrateFrom": "singlestat", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, "id": 6, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", "targets": [ { "datasource": { diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v22.table_panel_align.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v22.table_panel_align.v42.json index 22524901c13..d284282e97c 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v22.table_panel_align.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v22.table_panel_align.v42.json @@ -21,69 +21,25 @@ "links": [], "panels": [ { + "autoMigrateFrom": "table-old", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "custom.align" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Value" - }, - "properties": [ - { - "id": "custom.align" - } - ] - } - ] - }, "id": 1, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + "styles": [ + { + "align": "auto", + "pattern": "Time", + "type": "number" + }, + { + "align": "auto", + "pattern": "Value", + "type": "string" + } + ], "targets": [ { "datasource": { diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v24.table-angular.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v24.table-angular.v42.json index 420965dffb4..443ad1f6eb1 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v24.table-angular.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v24.table-angular.v42.json @@ -21,54 +21,30 @@ "links": [], "panels": [ { + "autoMigrateFrom": "table-old", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, "description": "Tests basic migration with default style pattern (/.*/) containing thresholds and colors. Should convert styles to fieldConfig.defaults with threshold steps.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": null - }, - { - "color": "yellow", - "value": 10 - }, - { - "color": "green", - "value": 20 - }, - { - "value": 30 - } - ] - } - }, - "overrides": [] - }, "id": 1, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + "legend": true, + "styles": [ + { + "colors": [ + "red", + "yellow", + "green" + ], + "pattern": "/.*/", + "thresholds": [ + "10", + "20", + "30" + ] + } + ], "targets": [ { "datasource": { @@ -91,136 +67,58 @@ "type": "table" }, { + "autoMigrateFrom": "table-old", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, "description": "Tests comprehensive migration including: default style with thresholds/colors/unit/decimals/align/colorMode, column overrides with exact name and regex patterns, date formatting, hidden columns, and links with tooltips.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "center", - "cellOptions": { - "type": "color-background" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, + "id": 2, + "styles": [ + { + "align": "center", + "colorMode": "cell", + "colors": [ + "green", + "yellow", + "red" + ], "decimals": 2, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "yellow", - "value": 100 - }, - { - "color": "red", - "value": 500 - } - ] - }, + "pattern": "/.*/", + "thresholds": [ + "100", + "500" + ], "unit": "bytes" }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Status" - }, - "properties": [ - { - "id": "displayName", - "value": "Current Status" - }, - { - "id": "unit", - "value": "short" - }, - { - "id": "decimals", - "value": 0 - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-text" - } - }, - { - "id": "custom.align", - "value": "left" - } - ] - }, - { - "matcher": { - "id": "byRegexp", - "options": "/Error.*/" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "targetBlank": true, - "title": "View error details", - "url": "http://example.com/errors" - } - ] - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-background" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "displayName", - "value": "Timestamp" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Hidden" - }, - "properties": [ - { - "id": "custom.hideFrom.viz", - "value": true - } - ] - } - ] - }, - "id": 2, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + { + "alias": "Current Status", + "align": "left", + "colorMode": "value", + "decimals": 0, + "pattern": "Status", + "unit": "short" + }, + { + "colorMode": "row", + "link": true, + "linkTargetBlank": true, + "linkTooltip": "View error details", + "linkUrl": "http://example.com/errors", + "pattern": "/Error.*/" + }, + { + "alias": "Timestamp", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "pattern": "Hidden", + "type": "hidden" + } + ], "targets": [ { "datasource": { @@ -235,49 +133,47 @@ "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [ + { + "text": "Average", + "value": "avg" + }, + { + "text": "Maximum", + "value": "max" + }, + { + "text": "Minimum", + "value": "min" + }, + { + "text": "Total", + "value": "total" + }, + { + "text": "Current", + "value": "current" + }, + { + "text": "Count", + "value": "count" + } + ], "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, "description": "Tests migration of timeseries_aggregations transform to reduce transformation with column mappings (avg-\u003emean, max-\u003emax, min-\u003emin, total-\u003esum, current-\u003elastNotNull, count-\u003ecount).", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 1, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "percent" - }, - "overrides": [] - }, "id": 3, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + "styles": [ + { + "decimals": 1, + "pattern": "/.*/", + "unit": "percent" + } + ], "targets": [ { "datasource": { @@ -289,67 +185,24 @@ } ], "title": "Table with Timeseries Aggregations Transform", - "transformations": [ - { - "id": "reduce", - "options": { - "includeTimeField": false, - "reducers": [ - "mean", - "max", - "min", - "sum", - "lastNotNull", - "count" - ] - } - } - ], + "transform": "timeseries_aggregations", "type": "table" }, { + "autoMigrateFrom": "table-old", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, "description": "Tests migration of timeseries_to_rows transform to seriesToRows transformation.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, "id": 4, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + "styles": [ + { + "pattern": "/.*/", + "unit": "short" + } + ], "targets": [ { "datasource": { @@ -361,59 +214,24 @@ } ], "title": "Table with Timeseries to Rows Transform", - "transformations": [ - { - "id": "seriesToRows", - "options": { - "reducers": [] - } - } - ], + "transform": "timeseries_to_rows", "type": "table" }, { + "autoMigrateFrom": "table-old", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, "description": "Tests migration of timeseries_to_columns transform to seriesToColumns transformation.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, "id": 5, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + "styles": [ + { + "pattern": "/.*/", + "unit": "bytes" + } + ], "targets": [ { "datasource": { @@ -425,57 +243,24 @@ } ], "title": "Table with Timeseries to Columns Transform", - "transformations": [ - { - "id": "seriesToColumns", - "options": { - "reducers": [] - } - } - ], + "transform": "timeseries_to_columns", "type": "table" }, { + "autoMigrateFrom": "table-old", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, "description": "Tests migration of table transform to merge transformation. Also tests auto alignment conversion to empty string.", - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, "id": 6, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + "styles": [ + { + "align": "auto", + "pattern": "/.*/" + } + ], "targets": [ { "datasource": { @@ -487,59 +272,24 @@ } ], "title": "Table with Merge Transform", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], + "transform": "table", "type": "table" }, { + "autoMigrateFrom": "table-old", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, "description": "Tests that existing transformations are preserved and new transformation from old format is appended to the list.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, "id": 7, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + "styles": [ + { + "pattern": "/.*/", + "unit": "short" + } + ], "targets": [ { "datasource": { @@ -551,6 +301,7 @@ } ], "title": "Table with Existing Transformations", + "transform": "timeseries_to_rows", "transformations": [ { "id": "filterFieldsByName", @@ -562,66 +313,35 @@ ] } } - }, - { - "id": "seriesToRows", - "options": { - "reducers": [] - } } ], "type": "table" }, { + "autoMigrateFrom": "table-old", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, "description": "Tests handling of mixed numeric and string threshold values (int, string, float) with proper type conversion.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "yellow", - "value": 10 - }, - { - "color": "orange", - "value": 20 - }, - { - "color": "red", - "value": 30.5 - } - ] - } - }, - "overrides": [] - }, "id": 8, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + "styles": [ + { + "colors": [ + "green", + "yellow", + "orange", + "red" + ], + "pattern": "/.*/", + "thresholds": [ + 10, + "20", + 30.5 + ] + } + ], "targets": [ { "datasource": { @@ -636,90 +356,32 @@ "type": "table" }, { + "autoMigrateFrom": "table-old", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, "description": "Tests all color mode mappings: cell-\u003ecolor-background, row-\u003ecolor-background, value-\u003ecolor-text.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "color-background" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "CellColumn" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "type": "color-background" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "RowColumn" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "type": "color-background" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "ValueColumn" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "type": "color-text" - } - } - ] - } - ] - }, "id": 9, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + "styles": [ + { + "colorMode": "cell", + "pattern": "/.*/" + }, + { + "colorMode": "cell", + "pattern": "CellColumn" + }, + { + "colorMode": "row", + "pattern": "RowColumn" + }, + { + "colorMode": "value", + "pattern": "ValueColumn" + } + ], "targets": [ { "datasource": { @@ -734,83 +396,32 @@ "type": "table" }, { + "autoMigrateFrom": "table-old", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, "description": "Tests all alignment options: left, center, right, and auto (should convert to empty string).", - "fieldConfig": { - "defaults": { - "custom": { - "align": "center", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "LeftColumn" - }, - "properties": [ - { - "id": "custom.align", - "value": "left" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "RightColumn" - }, - "properties": [ - { - "id": "custom.align", - "value": "right" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "AutoColumn" - }, - "properties": [ - { - "id": "custom.align" - } - ] - } - ] - }, "id": 10, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + "styles": [ + { + "align": "center", + "pattern": "/.*/" + }, + { + "align": "left", + "pattern": "LeftColumn" + }, + { + "align": "right", + "pattern": "RightColumn" + }, + { + "align": "auto", + "pattern": "AutoColumn" + } + ], "targets": [ { "datasource": { @@ -825,97 +436,36 @@ "type": "table" }, { + "autoMigrateFrom": "table-old", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, "description": "Tests both field matcher types: byName for exact matches and byRegexp for regex patterns.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, + "id": 11, + "styles": [ + { + "pattern": "/.*/", "unit": "short" }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "ExactColumnName" - }, - "properties": [ - { - "id": "displayName", - "value": "Exact Match" - } - ] - }, - { - "matcher": { - "id": "byRegexp", - "options": "/Regex.*Pattern/" - }, - "properties": [ - { - "id": "displayName", - "value": "Regex Match" - } - ] - }, - { - "matcher": { - "id": "byRegexp", - "options": "/^Start/" - }, - "properties": [ - { - "id": "displayName", - "value": "Start Pattern" - } - ] - }, - { - "matcher": { - "id": "byRegexp", - "options": "/End$/" - }, - "properties": [ - { - "id": "displayName", - "value": "End Pattern" - } - ] - } - ] - }, - "id": 11, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + { + "alias": "Exact Match", + "pattern": "ExactColumnName" + }, + { + "alias": "Regex Match", + "pattern": "/Regex.*Pattern/" + }, + { + "alias": "Start Pattern", + "pattern": "/^Start/" + }, + { + "alias": "End Pattern", + "pattern": "/End$/" + } + ], "targets": [ { "datasource": { @@ -930,103 +480,38 @@ "type": "table" }, { + "autoMigrateFrom": "table-old", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, "description": "Tests various link configurations: with and without tooltip, with and without target blank.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, + "id": 12, + "styles": [ + { + "pattern": "/.*/", "unit": "short" }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "LinkWithTooltip" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "targetBlank": true, - "title": "Click to view details", - "url": "http://example.com/with-tooltip" - } - ] - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "LinkWithoutTooltip" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "targetBlank": false, - "title": "", - "url": "http://example.com/no-tooltip" - } - ] - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "LinkMinimal" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "targetBlank": false, - "title": "", - "url": "http://example.com/minimal" - } - ] - } - ] - } - ] - }, - "id": 12, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + { + "link": true, + "linkTargetBlank": true, + "linkTooltip": "Click to view details", + "linkUrl": "http://example.com/with-tooltip", + "pattern": "LinkWithTooltip" + }, + { + "link": true, + "linkTargetBlank": false, + "linkUrl": "http://example.com/no-tooltip", + "pattern": "LinkWithoutTooltip" + }, + { + "link": true, + "linkUrl": "http://example.com/minimal", + "pattern": "LinkMinimal" + } + ], "targets": [ { "datasource": { @@ -1041,97 +526,38 @@ "type": "table" }, { + "autoMigrateFrom": "table-old", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, "description": "Tests various date format patterns and aliases.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, + "id": 13, + "styles": [ + { + "pattern": "/.*/", "unit": "short" }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "DateISO" - }, - "properties": [ - { - "id": "displayName", - "value": "ISO Date" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "DateTime" - }, - "properties": [ - { - "id": "displayName", - "value": "Full DateTime" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "TimeOnly" - }, - "properties": [ - { - "id": "displayName", - "value": "Time Only" - }, - { - "id": "unit", - "value": "time: HH:mm:ss" - } - ] - } - ] - }, - "id": 13, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + { + "alias": "ISO Date", + "dateFormat": "YYYY-MM-DD", + "pattern": "DateISO", + "type": "date" + }, + { + "alias": "Full DateTime", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "DateTime", + "type": "date" + }, + { + "alias": "Time Only", + "dateFormat": "HH:mm:ss", + "pattern": "TimeOnly", + "type": "date" + } + ], "targets": [ { "datasource": { @@ -1226,50 +652,14 @@ "type": "timeseries" }, { + "autoMigrateFrom": "singlestat", "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, "description": "Other panel types should not be affected by table migration.", - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, "id": 17, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", "targets": [ { "datasource": { diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v28.singlestat_and_variable_properties.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v28.singlestat_and_variable_properties.v42.json index 51ee2142199..efacbd5dc24 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v28.singlestat_and_variable_properties.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v28.singlestat_and_variable_properties.v42.json @@ -21,53 +21,23 @@ "links": [], "panels": [ { + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "#FF0000", - "value": null - }, - { - "color": "green", - "value": 10 - }, - { - "color": "orange", - "value": 20 - } - ] - } - }, - "overrides": [] + "grid": { + "max": 10, + "min": 1 }, "id": 1, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", + "legend": true, "targets": [ { "datasource": { @@ -86,56 +56,31 @@ "refId": "B" } ], + "thresholds": "10,20,30", "type": "stat" }, { + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "#FF0000", - "value": null - }, - { - "color": "green", - "value": 10 - }, - { - "color": "orange", - "value": 20 - } - ] - } - }, - "overrides": [] + "gauge": { + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "grid": { + "max": 10, + "min": 1 }, "id": 2, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", "targets": [ { "datasource": { @@ -146,82 +91,33 @@ "refId": "A" } ], + "thresholds": "10,20,30", "type": "stat" }, { + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, - "fieldConfig": { - "defaults": { - "mappings": [ - { - "options": { - "20": { - "text": "test" - } - }, - "type": "value" - }, - { - "options": { - "30": { - "text": "test1" - } - }, - "type": "value" - }, - { - "options": { - "40": { - "color": "orange", - "text": "50" - } - }, - "type": "value" - } - ], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "#FF0000", - "value": null - }, - { - "color": "green", - "value": 10 - }, - { - "color": "orange", - "value": 20 - } - ] - } - }, - "overrides": [] + "grid": { + "max": 10, + "min": 1 }, "id": 3, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", + "legend": true, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + } + ], "targets": [ { "datasource": { @@ -240,7 +136,25 @@ "refId": "B" } ], - "type": "stat" + "thresholds": "10,20,30", + "type": "stat", + "valueMaps": [ + { + "op": "=", + "text": "test", + "value": "20" + }, + { + "op": "=", + "text": "test1", + "value": "30" + }, + { + "op": "=", + "text": "50", + "value": "40" + } + ] }, { "datasource": { diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v28.singlestat_migration.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v28.singlestat_migration.v42.json index d541cf4751b..c08136b1926 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v28.singlestat_migration.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v28.singlestat_migration.v42.json @@ -21,53 +21,23 @@ "links": [], "panels": [ { + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "#FF0000", - "value": null - }, - { - "color": "green", - "value": 10 - }, - { - "color": "orange", - "value": 20 - } - ] - } - }, - "overrides": [] + "grid": { + "max": 10, + "min": 1 }, "id": 1, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", + "legend": true, "targets": [ { "datasource": { @@ -86,52 +56,27 @@ "refId": "B" } ], + "thresholds": "10,20,30", "type": "stat" }, { + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] + "grid": { + "max": 10, + "min": 1 }, "id": 2, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", + "legend": true, "targets": [ { "datasource": { @@ -150,56 +95,31 @@ "refId": "B" } ], + "thresholds": "", "type": "stat" }, { + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "#FF0000", - "value": null - }, - { - "color": "green", - "value": 10 - }, - { - "color": "orange", - "value": 20 - } - ] - } - }, - "overrides": [] + "gauge": { + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "grid": { + "max": 10, + "min": 1 }, "id": 3, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", "targets": [ { "datasource": { @@ -210,82 +130,33 @@ "refId": "A" } ], + "thresholds": "10,20,30", "type": "stat" }, { + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], "datasource": { "apiVersion": "v1", "type": "prometheus", "uid": "default-ds-uid" }, - "fieldConfig": { - "defaults": { - "mappings": [ - { - "options": { - "20": { - "text": "test" - } - }, - "type": "value" - }, - { - "options": { - "30": { - "text": "test1" - } - }, - "type": "value" - }, - { - "options": { - "40": { - "color": "orange", - "text": "50" - } - }, - "type": "value" - } - ], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "#FF0000", - "value": null - }, - { - "color": "green", - "value": 10 - }, - { - "color": "orange", - "value": 20 - } - ] - } - }, - "overrides": [] + "grid": { + "max": 10, + "min": 1 }, "id": 4, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", + "legend": true, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + } + ], "targets": [ { "datasource": { @@ -304,7 +175,25 @@ "refId": "B" } ], - "type": "stat" + "thresholds": "10,20,30", + "type": "stat", + "valueMaps": [ + { + "op": "=", + "text": "test", + "value": "20" + }, + { + "op": "=", + "text": "test1", + "value": "30" + }, + { + "op": "=", + "text": "50", + "value": "40" + } + ] }, { "datasource": { @@ -327,38 +216,24 @@ "type": "timeseries" }, { + "autoMigrateFrom": "grafana-singlestat-panel", + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], "datasource": { "type": "prometheus" }, - "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": "areaF2" - }, - "overrides": [] + "format": "areaF2", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true }, "gridPos": { "h": 8, @@ -367,25 +242,37 @@ "y": 43 }, "id": 5, - "maxDataPoints": 100, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "postfix": "b", + "postfixFontSize": "50%", + "prefix": "a", + "prefixFontSize": "50%", + "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 }, - "pluginVersion": "12.1.0", + "tableColumn": "", "targets": [ { "datasource": { @@ -395,8 +282,18 @@ "refId": "A" } ], + "thresholds": "", "title": "grafana-singlestat-panel", - "type": "stat" + "type": "stat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" }, { "datasource": { diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v13.graph_thresholds.v13.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v13.graph_thresholds.v13.json new file mode 100644 index 00000000000..62545df022a --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v13.graph_thresholds.v13.json @@ -0,0 +1,98 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "graph", + "grid": { + "threshold1": 200, + "threshold1Color": "yellow", + "threshold2": 400, + "threshold2Color": "red", + "thresholdLine": true + }, + "id": 1, + "title": "Graph with Line Thresholds", + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "grid": { + "threshold1": 100, + "threshold1Color": "green", + "threshold2": 300, + "threshold2Color": "blue", + "thresholdLine": false + }, + "id": 2, + "title": "Graph with Fill Thresholds", + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "grid": { + "threshold1": 150, + "threshold1Color": "orange", + "thresholdLine": true + }, + "id": 3, + "title": "Graph with Single Threshold", + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "grid": { + "threshold1": 200, + "threshold1Color": "yellow", + "thresholdLine": false + }, + "id": 4, + "thresholds": [ + { + "color": "purple", + "colorMode": "custom", + "value": 50 + } + ], + "title": "Graph with Existing Thresholds", + "type": "timeseries" + }, + { + "autoMigrateFrom": "singlestat", + "id": 5, + "title": "Non-Graph Panel", + "type": "stat" + } + ], + "schemaVersion": 13, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V13 Graph Thresholds Migration Test", + "weekStart": "" +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v13.minimal_graph_config.v13.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v13.minimal_graph_config.v13.json new file mode 100644 index 00000000000..ab364ee3075 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v13.minimal_graph_config.v13.json @@ -0,0 +1,56 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "graph", + "id": 4, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "editorMode": "builder", + "expr": "{\"a.utf8.metric 🤘\", job=\"prometheus-utf8\"}", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "type": "timeseries" + } + ], + "schemaVersion": 13, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Dashboard with minimal graph panel settings", + "weekStart": "" +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v24.table-angular.v24.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v24.table-angular.v24.json index 665c477506f..6091c413889 100644 --- a/apps/dashboard/pkg/migration/testdata/output/single_version/v24.table-angular.v24.json +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v24.table-angular.v24.json @@ -21,49 +21,25 @@ "links": [], "panels": [ { + "autoMigrateFrom": "table-old", "description": "Tests basic migration with default style pattern (/.*/) containing thresholds and colors. Should convert styles to fieldConfig.defaults with threshold steps.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": null - }, - { - "color": "yellow", - "value": 10 - }, - { - "color": "green", - "value": 20 - }, - { - "value": 30 - } - ] - } - }, - "overrides": [] - }, "id": 1, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + "legend": true, + "styles": [ + { + "colors": [ + "red", + "yellow", + "green" + ], + "pattern": "/.*/", + "thresholds": [ + "10", + "20", + "30" + ] + } + ], "targets": [ { "refId": "A" @@ -76,376 +52,151 @@ "type": "table" }, { + "autoMigrateFrom": "table-old", "description": "Tests comprehensive migration including: default style with thresholds/colors/unit/decimals/align/colorMode, column overrides with exact name and regex patterns, date formatting, hidden columns, and links with tooltips.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "center", - "cellOptions": { - "type": "color-background" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, + "id": 2, + "styles": [ + { + "align": "center", + "colorMode": "cell", + "colors": [ + "green", + "yellow", + "red" + ], "decimals": 2, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "yellow", - "value": 100 - }, - { - "color": "red", - "value": 500 - } - ] - }, + "pattern": "/.*/", + "thresholds": [ + "100", + "500" + ], "unit": "bytes" }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Status" - }, - "properties": [ - { - "id": "displayName", - "value": "Current Status" - }, - { - "id": "unit", - "value": "short" - }, - { - "id": "decimals", - "value": 0 - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-text" - } - }, - { - "id": "custom.align", - "value": "left" - } - ] - }, - { - "matcher": { - "id": "byRegexp", - "options": "/Error.*/" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "targetBlank": true, - "title": "View error details", - "url": "http://example.com/errors" - } - ] - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-background" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "displayName", - "value": "Timestamp" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Hidden" - }, - "properties": [ - { - "id": "custom.hideFrom.viz", - "value": true - } - ] - } - ] - }, - "id": 2, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + { + "alias": "Current Status", + "align": "left", + "colorMode": "value", + "decimals": 0, + "pattern": "Status", + "unit": "short" + }, + { + "colorMode": "row", + "link": true, + "linkTargetBlank": true, + "linkTooltip": "View error details", + "linkUrl": "http://example.com/errors", + "pattern": "/Error.*/" + }, + { + "alias": "Timestamp", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "pattern": "Hidden", + "type": "hidden" + } + ], "title": "Complex Table with All Style Features", "type": "table" }, { + "autoMigrateFrom": "table-old", + "columns": [ + { + "text": "Average", + "value": "avg" + }, + { + "text": "Maximum", + "value": "max" + }, + { + "text": "Minimum", + "value": "min" + }, + { + "text": "Total", + "value": "total" + }, + { + "text": "Current", + "value": "current" + }, + { + "text": "Count", + "value": "count" + } + ], "description": "Tests migration of timeseries_aggregations transform to reduce transformation with column mappings (avg-\u003emean, max-\u003emax, min-\u003emin, total-\u003esum, current-\u003elastNotNull, count-\u003ecount).", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "decimals": 1, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "percent" - }, - "overrides": [] - }, "id": 3, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + "styles": [ + { + "decimals": 1, + "pattern": "/.*/", + "unit": "percent" + } + ], "title": "Table with Timeseries Aggregations Transform", - "transformations": [ - { - "id": "reduce", - "options": { - "includeTimeField": false, - "reducers": [ - "mean", - "max", - "min", - "sum", - "lastNotNull", - "count" - ] - } - } - ], + "transform": "timeseries_aggregations", "type": "table" }, { + "autoMigrateFrom": "table-old", "description": "Tests migration of timeseries_to_rows transform to seriesToRows transformation.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, "id": 4, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", - "title": "Table with Timeseries to Rows Transform", - "transformations": [ + "styles": [ { - "id": "seriesToRows", - "options": { - "reducers": [] - } - } - ], - "type": "table" - }, - { - "description": "Tests migration of timeseries_to_columns transform to seriesToColumns transformation.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, - "id": 5, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", - "title": "Table with Timeseries to Columns Transform", - "transformations": [ - { - "id": "seriesToColumns", - "options": { - "reducers": [] - } - } - ], - "type": "table" - }, - { - "description": "Tests migration of table transform to merge transformation. Also tests auto alignment conversion to empty string.", - "fieldConfig": { - "defaults": { - "custom": { - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "id": 6, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", - "title": "Table with Merge Transform", - "transformations": [ - { - "id": "merge", - "options": { - "reducers": [] - } - } - ], - "type": "table" - }, - { - "description": "Tests that existing transformations are preserved and new transformation from old format is appended to the list.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, + "pattern": "/.*/", "unit": "short" - }, - "overrides": [] - }, + } + ], + "title": "Table with Timeseries to Rows Transform", + "transform": "timeseries_to_rows", + "type": "table" + }, + { + "autoMigrateFrom": "table-old", + "description": "Tests migration of timeseries_to_columns transform to seriesToColumns transformation.", + "id": 5, + "styles": [ + { + "pattern": "/.*/", + "unit": "bytes" + } + ], + "title": "Table with Timeseries to Columns Transform", + "transform": "timeseries_to_columns", + "type": "table" + }, + { + "autoMigrateFrom": "table-old", + "description": "Tests migration of table transform to merge transformation. Also tests auto alignment conversion to empty string.", + "id": 6, + "styles": [ + { + "align": "auto", + "pattern": "/.*/" + } + ], + "title": "Table with Merge Transform", + "transform": "table", + "type": "table" + }, + { + "autoMigrateFrom": "table-old", + "description": "Tests that existing transformations are preserved and new transformation from old format is appended to the list.", "id": 7, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + "styles": [ + { + "pattern": "/.*/", + "unit": "short" + } + ], "title": "Table with Existing Transformations", + "transform": "timeseries_to_rows", "transformations": [ { "id": "filterFieldsByName", @@ -457,496 +208,171 @@ ] } } - }, - { - "id": "seriesToRows", - "options": { - "reducers": [] - } } ], "type": "table" }, { + "autoMigrateFrom": "table-old", "description": "Tests handling of mixed numeric and string threshold values (int, string, float) with proper type conversion.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "yellow", - "value": 10 - }, - { - "color": "orange", - "value": 20 - }, - { - "color": "red", - "value": 30.5 - } - ] - } - }, - "overrides": [] - }, "id": 8, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + "styles": [ + { + "colors": [ + "green", + "yellow", + "orange", + "red" + ], + "pattern": "/.*/", + "thresholds": [ + 10, + "20", + 30.5 + ] + } + ], "title": "Mixed Threshold Types", "type": "table" }, { + "autoMigrateFrom": "table-old", "description": "Tests all color mode mappings: cell-\u003ecolor-background, row-\u003ecolor-background, value-\u003ecolor-text.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "color-background" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "CellColumn" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "type": "color-background" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "RowColumn" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "type": "color-background" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "ValueColumn" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "type": "color-text" - } - } - ] - } - ] - }, "id": 9, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + "styles": [ + { + "colorMode": "cell", + "pattern": "/.*/" + }, + { + "colorMode": "cell", + "pattern": "CellColumn" + }, + { + "colorMode": "row", + "pattern": "RowColumn" + }, + { + "colorMode": "value", + "pattern": "ValueColumn" + } + ], "title": "All Color Modes Test", "type": "table" }, { + "autoMigrateFrom": "table-old", "description": "Tests all alignment options: left, center, right, and auto (should convert to empty string).", - "fieldConfig": { - "defaults": { - "custom": { - "align": "center", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "LeftColumn" - }, - "properties": [ - { - "id": "custom.align", - "value": "left" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "RightColumn" - }, - "properties": [ - { - "id": "custom.align", - "value": "right" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "AutoColumn" - }, - "properties": [ - { - "id": "custom.align" - } - ] - } - ] - }, "id": 10, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + "styles": [ + { + "align": "center", + "pattern": "/.*/" + }, + { + "align": "left", + "pattern": "LeftColumn" + }, + { + "align": "right", + "pattern": "RightColumn" + }, + { + "align": "auto", + "pattern": "AutoColumn" + } + ], "title": "All Alignment Options", "type": "table" }, { + "autoMigrateFrom": "table-old", "description": "Tests both field matcher types: byName for exact matches and byRegexp for regex patterns.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, + "id": 11, + "styles": [ + { + "pattern": "/.*/", "unit": "short" }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "ExactColumnName" - }, - "properties": [ - { - "id": "displayName", - "value": "Exact Match" - } - ] - }, - { - "matcher": { - "id": "byRegexp", - "options": "/Regex.*Pattern/" - }, - "properties": [ - { - "id": "displayName", - "value": "Regex Match" - } - ] - }, - { - "matcher": { - "id": "byRegexp", - "options": "/^Start/" - }, - "properties": [ - { - "id": "displayName", - "value": "Start Pattern" - } - ] - }, - { - "matcher": { - "id": "byRegexp", - "options": "/End$/" - }, - "properties": [ - { - "id": "displayName", - "value": "End Pattern" - } - ] - } - ] - }, - "id": 11, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + { + "alias": "Exact Match", + "pattern": "ExactColumnName" + }, + { + "alias": "Regex Match", + "pattern": "/Regex.*Pattern/" + }, + { + "alias": "Start Pattern", + "pattern": "/^Start/" + }, + { + "alias": "End Pattern", + "pattern": "/End$/" + } + ], "title": "Field Matcher Types Test", "type": "table" }, { + "autoMigrateFrom": "table-old", "description": "Tests various link configurations: with and without tooltip, with and without target blank.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, + "id": 12, + "styles": [ + { + "pattern": "/.*/", "unit": "short" }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "LinkWithTooltip" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "targetBlank": true, - "title": "Click to view details", - "url": "http://example.com/with-tooltip" - } - ] - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "LinkWithoutTooltip" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "targetBlank": false, - "title": "", - "url": "http://example.com/no-tooltip" - } - ] - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "LinkMinimal" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "targetBlank": false, - "title": "", - "url": "http://example.com/minimal" - } - ] - } - ] - } - ] - }, - "id": 12, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + { + "link": true, + "linkTargetBlank": true, + "linkTooltip": "Click to view details", + "linkUrl": "http://example.com/with-tooltip", + "pattern": "LinkWithTooltip" + }, + { + "link": true, + "linkTargetBlank": false, + "linkUrl": "http://example.com/no-tooltip", + "pattern": "LinkWithoutTooltip" + }, + { + "link": true, + "linkUrl": "http://example.com/minimal", + "pattern": "LinkMinimal" + } + ], "title": "Link Configuration Test", "type": "table" }, { + "autoMigrateFrom": "table-old", "description": "Tests various date format patterns and aliases.", - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "footer": { - "reducers": [] - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, + "id": 13, + "styles": [ + { + "pattern": "/.*/", "unit": "short" }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "DateISO" - }, - "properties": [ - { - "id": "displayName", - "value": "ISO Date" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "DateTime" - }, - "properties": [ - { - "id": "displayName", - "value": "Full DateTime" - }, - { - "id": "unit", - "value": "time: YYYY-MM-DD HH:mm:ss" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "TimeOnly" - }, - "properties": [ - { - "id": "displayName", - "value": "Time Only" - }, - { - "id": "unit", - "value": "time: HH:mm:ss" - } - ] - } - ] - }, - "id": 13, - "options": { - "cellHeight": "sm", - "showHeader": true - }, - "pluginVersion": "12.1.0", + { + "alias": "ISO Date", + "dateFormat": "YYYY-MM-DD", + "pattern": "DateISO", + "type": "date" + }, + { + "alias": "Full DateTime", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "DateTime", + "type": "date" + }, + { + "alias": "Time Only", + "dateFormat": "HH:mm:ss", + "pattern": "TimeOnly", + "type": "date" + } + ], "title": "Date Format Variations", "type": "table" }, diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v28.singlestat_and_variable_properties.v28.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v28.singlestat_and_variable_properties.v28.json index ba43b6785d3..2b7b8e83aa3 100644 --- a/apps/dashboard/pkg/migration/testdata/output/single_version/v28.singlestat_and_variable_properties.v28.json +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v28.singlestat_and_variable_properties.v28.json @@ -21,48 +21,18 @@ "links": [], "panels": [ { - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "#FF0000", - "value": null - }, - { - "color": "green", - "value": 10 - }, - { - "color": "orange", - "value": 20 - } - ] - } - }, - "overrides": [] + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], + "grid": { + "max": 10, + "min": 1 }, "id": 1, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", + "legend": true, "targets": [ { "refId": "A" @@ -71,122 +41,48 @@ "refId": "B" } ], + "thresholds": "10,20,30", "type": "stat" }, { - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "#FF0000", - "value": null - }, - { - "color": "green", - "value": 10 - }, - { - "color": "orange", - "value": 20 - } - ] - } - }, - "overrides": [] + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], + "gauge": { + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "grid": { + "max": 10, + "min": 1 }, "id": 2, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", + "thresholds": "10,20,30", "type": "stat" }, { - "fieldConfig": { - "defaults": { - "mappings": [ - { - "options": { - "20": { - "text": "test" - } - }, - "type": "value" - }, - { - "options": { - "30": { - "text": "test1" - } - }, - "type": "value" - }, - { - "options": { - "40": { - "color": "orange", - "text": "50" - } - }, - "type": "value" - } - ], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "#FF0000", - "value": null - }, - { - "color": "green", - "value": 10 - }, - { - "color": "orange", - "value": 20 - } - ] - } - }, - "overrides": [] + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], + "grid": { + "max": 10, + "min": 1 }, "id": 3, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", + "legend": true, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + } + ], "targets": [ { "refId": "A" @@ -195,7 +91,25 @@ "refId": "B" } ], - "type": "stat" + "thresholds": "10,20,30", + "type": "stat", + "valueMaps": [ + { + "op": "=", + "text": "test", + "value": "20" + }, + { + "op": "=", + "text": "test1", + "value": "30" + }, + { + "op": "=", + "text": "50", + "value": "40" + } + ] }, { "id": 4, diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v28.singlestat_migration.v28.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v28.singlestat_migration.v28.json index 55a93c8a0f1..77f8ce45a63 100644 --- a/apps/dashboard/pkg/migration/testdata/output/single_version/v28.singlestat_migration.v28.json +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v28.singlestat_migration.v28.json @@ -21,48 +21,18 @@ "links": [], "panels": [ { - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "#FF0000", - "value": null - }, - { - "color": "green", - "value": 10 - }, - { - "color": "orange", - "value": 20 - } - ] - } - }, - "overrides": [] + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], + "grid": { + "max": 10, + "min": 1 }, "id": 1, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", + "legend": true, "targets": [ { "refId": "A" @@ -71,47 +41,22 @@ "refId": "B" } ], + "thresholds": "10,20,30", "type": "stat" }, { - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], + "grid": { + "max": 10, + "min": 1 }, "id": 2, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", + "legend": true, "targets": [ { "refId": "A" @@ -120,122 +65,48 @@ "refId": "B" } ], + "thresholds": "", "type": "stat" }, { - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "#FF0000", - "value": null - }, - { - "color": "green", - "value": 10 - }, - { - "color": "orange", - "value": 20 - } - ] - } - }, - "overrides": [] + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], + "gauge": { + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "grid": { + "max": 10, + "min": 1 }, "id": 3, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", + "thresholds": "10,20,30", "type": "stat" }, { - "fieldConfig": { - "defaults": { - "mappings": [ - { - "options": { - "20": { - "text": "test" - } - }, - "type": "value" - }, - { - "options": { - "30": { - "text": "test1" - } - }, - "type": "value" - }, - { - "options": { - "40": { - "color": "orange", - "text": "50" - } - }, - "type": "value" - } - ], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "#FF0000", - "value": null - }, - { - "color": "green", - "value": 10 - }, - { - "color": "orange", - "value": 20 - } - ] - } - }, - "overrides": [] + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], + "grid": { + "max": 10, + "min": 1 }, "id": 4, - "options": { - "colorMode": "none", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", + "legend": true, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + } + ], "targets": [ { "refId": "A" @@ -244,7 +115,25 @@ "refId": "B" } ], - "type": "stat" + "thresholds": "10,20,30", + "type": "stat", + "valueMaps": [ + { + "op": "=", + "text": "test", + "value": "20" + }, + { + "op": "=", + "text": "test1", + "value": "30" + }, + { + "op": "=", + "text": "50", + "value": "40" + } + ] }, { "id": 8, @@ -257,38 +146,24 @@ "type": "timeseries" }, { + "autoMigrateFrom": "grafana-singlestat-panel", + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], "datasource": { "type": "prometheus" }, - "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": "areaF2" - }, - "overrides": [] + "format": "areaF2", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true }, "gridPos": { "h": 8, @@ -297,25 +172,37 @@ "y": 43 }, "id": 5, - "maxDataPoints": 100, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "horizontal", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "postfix": "b", + "postfixFontSize": "50%", + "prefix": "a", + "prefixFontSize": "50%", + "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 }, - "pluginVersion": "12.1.0", + "tableColumn": "", "targets": [ { "datasource": { @@ -325,8 +212,18 @@ "refId": "A" } ], + "thresholds": "", "title": "grafana-singlestat-panel", - "type": "stat" + "type": "stat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" }, { "datasource": { diff --git a/eslint-suppressions.json b/eslint-suppressions.json index c69363ff210..7e58c1ec3c3 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -2541,10 +2541,10 @@ }, "public/app/features/dashboard/state/DashboardMigrator.ts": { "@typescript-eslint/consistent-type-assertions": { - "count": 4 + "count": 2 }, "@typescript-eslint/no-explicit-any": { - "count": 24 + "count": 20 } }, "public/app/features/dashboard/state/DashboardModel.repeat.test.ts": { diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index d46a96066f9..c18fd24a6f8 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -1,4 +1,4 @@ -import { each, find, findIndex, flattenDeep, isArray, isBoolean, isNumber, isString, map, max, some } from 'lodash'; +import { each, find, findIndex, flattenDeep, isArray, isBoolean, isString, map, max, some } from 'lodash'; import { AnnotationQuery, @@ -15,7 +15,6 @@ import { isDataSourceRef, isEmptyObject, MappingType, - PanelPlugin, ReducerID, SpecialValueMatch, standardEditorsRegistry, @@ -33,7 +32,6 @@ import { DataTransformerConfig } from '@grafana/schema'; import { AxisPlacement, GraphFieldConfig } from '@grafana/ui'; import { migrateTableDisplayModeToCellOptions } from '@grafana/ui/internal'; import { getAllOptionEditors, getAllStandardFieldConfigs } from 'app/core/components/OptionsUI/registry'; -import { config } from 'app/core/config'; import { DEFAULT_PANEL_SPAN, DEFAULT_ROW_HEIGHT, @@ -53,8 +51,6 @@ import { isConstant, isMulti } from 'app/features/variables/guard'; import { alignCurrentWithMulti } from 'app/features/variables/shared/multiOptions'; import { CloudWatchMetricsQuery, LegacyAnnotationQuery } from 'app/plugins/datasource/cloudwatch/types'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; -import { plugin as gaugePanelPlugin } from 'app/plugins/panel/gauge/module'; -import { plugin as statPanelPlugin } from 'app/plugins/panel/stat/module'; import { migrateCloudWatchQuery, @@ -403,72 +399,8 @@ export class DashboardMigrator { } if (oldVersion < 13 && finalTargetVersion >= 13) { - // update graph yaxes changes - panelUpgrades.push((panel: any) => { - if (panel.type !== 'graph') { - return panel; - } - if (!panel.grid) { - return panel; - } - - if (!panel.thresholds) { - panel.thresholds = []; - } - const t1: any = {}, - t2: any = {}; - - if (panel.grid.threshold1 !== null) { - t1.value = panel.grid.threshold1; - if (panel.grid.thresholdLine) { - t1.line = true; - t1.lineColor = panel.grid.threshold1Color; - t1.colorMode = 'custom'; - } else { - t1.fill = true; - t1.fillColor = panel.grid.threshold1Color; - t1.colorMode = 'custom'; - } - } - - if (panel.grid.threshold2 !== null) { - t2.value = panel.grid.threshold2; - if (panel.grid.thresholdLine) { - t2.line = true; - t2.lineColor = panel.grid.threshold2Color; - t2.colorMode = 'custom'; - } else { - t2.fill = true; - t2.fillColor = panel.grid.threshold2Color; - t2.colorMode = 'custom'; - } - } - - if (isNumber(t1.value)) { - if (isNumber(t2.value)) { - if (t1.value > t2.value) { - t1.op = t2.op = 'lt'; - panel.thresholds.push(t1); - panel.thresholds.push(t2); - } else { - t1.op = t2.op = 'gt'; - panel.thresholds.push(t1); - panel.thresholds.push(t2); - } - } else { - t1.op = 'gt'; - panel.thresholds.push(t1); - } - } - - delete panel.grid.threshold1; - delete panel.grid.threshold1Color; - delete panel.grid.threshold2; - delete panel.grid.threshold2Color; - delete panel.grid.thresholdLine; - - return panel; - }); + // graph panel auto migrates to either barchart, bargauge, histogram or timeseries (all standard Grafana plugins) + // see public/app/features/dashboard/state/getPanelPluginToMigrateTo.ts } if (oldVersion < 14 && finalTargetVersion >= 14) { @@ -693,14 +625,6 @@ export class DashboardMigrator { } if (oldVersion < 28 && finalTargetVersion >= 28) { - panelUpgrades.push((panel: PanelModel) => { - if (panel.type === 'singlestat') { - return migrateSinglestat(panel); - } - - return panel; - }); - for (const variable of this.dashboard.templating.list) { if (variable.tags) { delete variable.tags; @@ -1265,40 +1189,6 @@ function updateVariablesSyntax(text: string) { }); } -function migrateSinglestat(panel: PanelModel) { - // If 'grafana-singlestat-panel' exists, move to that - if (config.panels['grafana-singlestat-panel']) { - panel.type = 'grafana-singlestat-panel'; - return panel; - } - - let returnSaveModel = false; - - if (!panel.changePlugin) { - returnSaveModel = true; - panel = new PanelModel(panel); - } - - // To make sure PanelModel.isAngularPlugin logic thinks the current panel is angular - // And since this plugin no longer exist we just fake it here - panel.plugin = { angularPanelCtrl: {} } as PanelPlugin; - - // Otheriwse use gauge or stat panel - if ((panel as any).gauge?.show) { - gaugePanelPlugin.meta = config.panels['gauge']; - panel.changePlugin(gaugePanelPlugin); - } else { - statPanelPlugin.meta = config.panels['stat']; - panel.changePlugin(statPanelPlugin); - } - - if (returnSaveModel) { - return panel.getSaveModel(); - } - - return panel; -} - interface MigrateDatasourceNameOptions { returnDefaultAsNull: boolean; } diff --git a/public/app/features/dashboard/state/DashboardMigratorSingleVersion.test.ts b/public/app/features/dashboard/state/DashboardMigratorSingleVersion.test.ts index 1bebed4f787..922a3c70ccd 100644 --- a/public/app/features/dashboard/state/DashboardMigratorSingleVersion.test.ts +++ b/public/app/features/dashboard/state/DashboardMigratorSingleVersion.test.ts @@ -18,7 +18,6 @@ import { getJsonInputFiles, extractTargetVersionFromFilename, constructBackendOutputFilename, - handleAngularPanelMigration, } from './__tests__/migrationTestUtils'; /* @@ -109,9 +108,6 @@ describe('Backend / Frontend single version migration result comparison', () => getVariablesFromState: () => jsonInput?.templating?.list ?? [], }); - // Handle angular panel migration if needed - await handleAngularPanelMigration(frontendModel, jsonInput.schemaVersion, targetVersion); - const frontendMigrationResult = frontendModel.getSaveModelClone(); // version in the backend is never added because it is returned from the backend as metadata diff --git a/public/app/features/dashboard/state/DashboardMigratorToBackend.devDashboards.test.ts b/public/app/features/dashboard/state/DashboardMigratorToBackend.devDashboards.test.ts index 09b69eb985d..ccd3f11b9b0 100644 --- a/public/app/features/dashboard/state/DashboardMigratorToBackend.devDashboards.test.ts +++ b/public/app/features/dashboard/state/DashboardMigratorToBackend.devDashboards.test.ts @@ -12,11 +12,7 @@ import { createTextBoxVariableAdapter } from 'app/features/variables/textbox/ada import { DASHBOARD_SCHEMA_VERSION } from './DashboardMigrator'; import { DashboardModel } from './DashboardModel'; -import { - setupDevDashboardDataSources, - handleAngularPanelMigration, - constructLatestVersionOutputFilename, -} from './__tests__/migrationTestUtils'; +import { setupDevDashboardDataSources, constructLatestVersionOutputFilename } from './__tests__/migrationTestUtils'; /* * Dev Dashboard Backend / Frontend Migration Comparison Test @@ -107,9 +103,6 @@ describe('Dev Dashboard Backend / Frontend result comparison', () => { getVariablesFromState: () => jsonInput?.templating?.list ?? [], }); - // Handle angular panel migration if needed - await handleAngularPanelMigration(frontendModel, jsonInput.schemaVersion, DASHBOARD_SCHEMA_VERSION); - const frontendMigrationResult = frontendModel.getSaveModelClone(); // version in the backend is never added because it is returned from the backend as metadata diff --git a/public/app/features/dashboard/state/DashboardMigratorToBackend.test.ts b/public/app/features/dashboard/state/DashboardMigratorToBackend.test.ts index 76ac9140d7a..bef909ca13c 100644 --- a/public/app/features/dashboard/state/DashboardMigratorToBackend.test.ts +++ b/public/app/features/dashboard/state/DashboardMigratorToBackend.test.ts @@ -17,7 +17,6 @@ import { getOutputDirectory, getJsonInputFiles, constructLatestVersionOutputFilename, - handleAngularPanelMigration, } from './__tests__/migrationTestUtils'; /* @@ -76,9 +75,6 @@ describe('Backend / Frontend result comparison', () => { getVariablesFromState: () => jsonInput?.templating?.list ?? [], }); - // Handle angular panel migration if needed - await handleAngularPanelMigration(frontendModel, jsonInput.schemaVersion, DASHBOARD_SCHEMA_VERSION); - const frontendMigrationResult = frontendModel.getSaveModelClone(); // version in the backend is never added because it is returned from the backend as metadata diff --git a/public/app/features/dashboard/state/__tests__/migrationTestUtils.ts b/public/app/features/dashboard/state/__tests__/migrationTestUtils.ts index bbbc9d83f6a..60feacaaf7e 100644 --- a/public/app/features/dashboard/state/__tests__/migrationTestUtils.ts +++ b/public/app/features/dashboard/state/__tests__/migrationTestUtils.ts @@ -1,14 +1,9 @@ import { readdirSync } from 'fs'; import path from 'path'; -import { PanelPlugin } from '@grafana/data'; import { mockDataSource } from 'app/features/alerting/unified/mocks'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; -import { plugin as statPanelPlugin } from 'app/plugins/panel/stat/module'; -import { plugin as tablePanelPlugin } from 'app/plugins/panel/table/module'; - -import { DashboardModel } from '../DashboardModel'; // Set up the same datasources as backend test provider to ensure consistency export const dataSources = { @@ -182,71 +177,3 @@ export function constructBackendOutputFilename(inputFile: string, targetVersion: export function constructLatestVersionOutputFilename(inputFile: string, latestVersion: number): string { return inputFile.replace('.json', `.v${latestVersion}.json`); } - -export const pluginVersionForAutoMigrate = '12.1.0'; - -/** - * Creates a type-compatible PanelPlugin wrapper for the real panel plugins - * This ensures the plugin has the correct version set and is compatible with pluginLoaded method - */ -function getPanelPlugin(pluginId: 'stat' | 'table'): PanelPlugin { - const realPlugin = pluginId === 'stat' ? statPanelPlugin : tablePanelPlugin; - - // Create a copy of the plugin to avoid modifying the original - const pluginCopy = Object.create(Object.getPrototypeOf(realPlugin)); - Object.assign(pluginCopy, realPlugin); - - // Ensure meta and info exist - if (!pluginCopy.meta.info) { - pluginCopy.meta.info = { - author: { name: 'Grafana Labs', url: 'https://grafana.com' }, - description: `${pluginId} panel plugin`, - links: [], - logos: { small: '', large: '' }, - screenshots: [], - updated: '2024-01-01', - version: pluginVersionForAutoMigrate, - }; - } else { - // Ensure version is set - pluginCopy.meta.info.version = pluginVersionForAutoMigrate; - } - - return pluginCopy; -} - -export async function handleAngularPanelMigration( - frontendModel: DashboardModel, - sourceVersion: number, - targetVersion: number -): Promise { - /* - Migration from schema V27 involves migrating angular singlestat panels to stat panels - These panels are auto migrated where PanelModel.restoreModel() is called in the constructor, - and the autoMigrateFrom is set and type is set to "stat". So this logic will not run. - if (oldVersion < 28) { - panelUpgrades.push((panel: PanelModel) => { - if (panel.type === 'singlestat') { - return migrateSinglestat(panel); - } - }); - } - - Furthermore, the PanelModel.pluginLoaded is run in the old architecture through a redux action so it will not run in this test. - In the scenes architecture the angular migration logic runs through a migration handler inside transformSaveModelToScene.ts - _UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel), - We need to manually run the pluginLoaded logic to ensure the panels are migrated correctly. - which means that the actual migration logic is not run. - We need to manually run the pluginLoaded logic to ensure the panels are migrated correctly. - */ - for (const panel of frontendModel.panels) { - if (panel.type === 'stat' && panel.autoMigrateFrom && targetVersion >= 28 && sourceVersion < 28) { - const statPlugin = getPanelPlugin('stat'); - await panel.pluginLoaded(statPlugin); - } - if (panel.type === 'table' && panel.autoMigrateFrom === 'table-old' && targetVersion >= 24 && sourceVersion < 24) { - const tablePlugin = getPanelPlugin('table'); - await panel.pluginLoaded(tablePlugin); - } - } -} From 1f7f3c9a5a1adceb73040184cd897af1e5124972 Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Thu, 9 Oct 2025 11:25:39 -0600 Subject: [PATCH 104/578] Unified Storage: KVStore use new validation (#110834) * name field must match either k8s regex or grafana legacy uid regex. Adds tests. * moves invalid test to being valid * uses new US naming validation for kv store validation * fix function name and update key regex * fix comment * use correct errs var * updates kv key tests --- pkg/apimachinery/validation/validation.go | 2 +- .../validation/validation_test.go | 2 +- pkg/storage/unified/resource/datastore.go | 101 ++- .../unified/resource/datastore_test.go | 602 ++++++++++++------ pkg/storage/unified/resource/errors.go | 23 + pkg/storage/unified/resource/eventstore.go | 49 +- pkg/storage/unified/resource/keys.go | 2 +- pkg/storage/unified/resource/kv.go | 4 +- pkg/storage/unified/resource/kv_test.go | 6 +- pkg/storage/unified/resource/server.go | 2 +- 10 files changed, 502 insertions(+), 291 deletions(-) diff --git a/pkg/apimachinery/validation/validation.go b/pkg/apimachinery/validation/validation.go index 777eb59fa46..6cdbb6b7e78 100644 --- a/pkg/apimachinery/validation/validation.go +++ b/pkg/apimachinery/validation/validation.go @@ -90,7 +90,7 @@ func IsValidGroup(group string) []string { // If the value is not valid, a list of error strings is returned. // Otherwise an empty list (or nil) is returned. -func IsValidateResource(resource string) []string { +func IsValidResource(resource string) []string { s := len(resource) switch { case s > maxResourceLength: diff --git a/pkg/apimachinery/validation/validation_test.go b/pkg/apimachinery/validation/validation_test.go index 3bb5fa61ad9..380f69fd362 100644 --- a/pkg/apimachinery/validation/validation_test.go +++ b/pkg/apimachinery/validation/validation_test.go @@ -222,7 +222,7 @@ func TestValidation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { for _, input := range tt.input { - output := validation.IsValidateResource(input) + output := validation.IsValidResource(input) require.Equal(t, tt.expect, output, "input: %s", input) } }) diff --git a/pkg/storage/unified/resource/datastore.go b/pkg/storage/unified/resource/datastore.go index e45e1b74101..d5fc0cdf915 100644 --- a/pkg/storage/unified/resource/datastore.go +++ b/pkg/storage/unified/resource/datastore.go @@ -2,15 +2,16 @@ package resource import ( "context" + "errors" "fmt" "io" "iter" "math" - "regexp" "strconv" "strings" "time" + "github.com/grafana/grafana/pkg/apimachinery/validation" gocache "github.com/patrickmn/go-cache" ) @@ -54,12 +55,6 @@ type GroupResource struct { Resource string } -var ( - // validNameRegex validates that a name contains only lowercase alphanumeric characters, '-' or '.' - // and starts and ends with an alphanumeric character - validNameRegex = regexp.MustCompile(`^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$`) -) - func (k DataKey) String() string { return fmt.Sprintf("%s/%s/%s/%s/%d~%s~%s", k.Group, k.Resource, k.Namespace, k.Name, k.ResourceVersion, k.Action, k.Folder) } @@ -69,42 +64,35 @@ func (k DataKey) Equals(other DataKey) bool { } func (k DataKey) Validate() error { - if k.Group == "" { - return fmt.Errorf("group is required") - } - if k.Resource == "" { - return fmt.Errorf("resource is required") - } if k.Namespace == "" { - return fmt.Errorf("namespace is required") - } - if k.Name == "" { - return fmt.Errorf("name is required") + return NewValidationError("namespace", k.Namespace, ErrNamespaceRequired) } if k.ResourceVersion <= 0 { - return fmt.Errorf("resource version must be positive") + return NewValidationError("resourceVersion", fmt.Sprintf("%d", k.ResourceVersion), ErrResourceVersionInvalid) } if k.Action == "" { - return fmt.Errorf("action is required") + return NewValidationError("action", string(k.Action), ErrActionRequired) } // Validate naming conventions for all required fields - if !validNameRegex.MatchString(k.Namespace) { - return fmt.Errorf("namespace '%s' is invalid", k.Namespace) + if err := validation.IsValidNamespace(k.Namespace); err != nil { + return NewValidationError("namespace", k.Namespace, err[0]) } - if !validNameRegex.MatchString(k.Group) { - return fmt.Errorf("group '%s' is invalid", k.Group) + if err := validation.IsValidGroup(k.Group); err != nil { + return NewValidationError("group", k.Group, err[0]) } - if !validNameRegex.MatchString(k.Resource) { - return fmt.Errorf("resource '%s' is invalid", k.Resource) + if err := validation.IsValidResource(k.Resource); err != nil { + return NewValidationError("resource", k.Resource, err[0]) } - if !validNameRegex.MatchString(k.Name) { - return fmt.Errorf("name '%s' is invalid", k.Name) + if err := validation.IsValidGrafanaName(k.Name); err != nil { + return NewValidationError("name", k.Name, err[0]) } // Validate folder field if provided (optional field) - if k.Folder != "" && !validNameRegex.MatchString(k.Folder) { - return fmt.Errorf("folder '%s' is invalid", k.Folder) + if k.Folder != "" { + if err := validation.IsValidGrafanaName(k.Folder); err != nil { + return NewValidationError("folder", k.Folder, err[0]) + } } // Validate action is one of the valid values @@ -124,27 +112,21 @@ type ListRequestKey struct { } func (k ListRequestKey) Validate() error { - if k.Group == "" { - return fmt.Errorf("group is required") - } - if k.Resource == "" { - return fmt.Errorf("resource is required") - } if k.Namespace == "" && k.Name != "" { - return fmt.Errorf("name must be empty when namespace is empty") + return errors.New(ErrNameMustBeEmptyWhenNamespaceEmpty) } - if k.Namespace != "" && !validNameRegex.MatchString(k.Namespace) { - return fmt.Errorf("namespace '%s' is invalid", k.Namespace) + if k.Namespace != "" { + if err := validation.IsValidNamespace(k.Namespace); err != nil { + return NewValidationError("namespace", k.Namespace, err[0]) + } } - if !validNameRegex.MatchString(k.Group) { - return fmt.Errorf("group '%s' is invalid", k.Group) + if err := validation.IsValidGroup(k.Group); err != nil { + return NewValidationError("group", k.Group, err[0]) } - if !validNameRegex.MatchString(k.Resource) { - return fmt.Errorf("resource '%s' is invalid", k.Resource) - } - if k.Name != "" && !validNameRegex.MatchString(k.Name) { - return fmt.Errorf("name '%s' is invalid", k.Name) + if err := validation.IsValidResource(k.Resource); err != nil { + return NewValidationError("resource", k.Resource, err[0]) } + return nil } @@ -168,31 +150,20 @@ type GetRequestKey struct { // Validate validates the get request key func (k GetRequestKey) Validate() error { - if k.Group == "" { - return fmt.Errorf("group is required") - } - if k.Resource == "" { - return fmt.Errorf("resource is required") - } if k.Namespace == "" { - return fmt.Errorf("namespace is required") + return errors.New(ErrNamespaceRequired) } - if k.Name == "" { - return fmt.Errorf("name is required") + if err := validation.IsValidNamespace(k.Namespace); err != nil { + return NewValidationError("namespace", k.Namespace, err[0]) } - - // Validate naming conventions - if !validNameRegex.MatchString(k.Namespace) { - return fmt.Errorf("namespace '%s' is invalid", k.Namespace) + if err := validation.IsValidGroup(k.Group); err != nil { + return NewValidationError("group", k.Group, err[0]) } - if !validNameRegex.MatchString(k.Group) { - return fmt.Errorf("group '%s' is invalid", k.Group) + if err := validation.IsValidResource(k.Resource); err != nil { + return NewValidationError("resource", k.Resource, err[0]) } - if !validNameRegex.MatchString(k.Resource) { - return fmt.Errorf("resource '%s' is invalid", k.Resource) - } - if !validNameRegex.MatchString(k.Name) { - return fmt.Errorf("name '%s' is invalid", k.Name) + if err := validation.IsValidGrafanaName(k.Name); err != nil { + return NewValidationError("name", k.Name, err[0]) } return nil diff --git a/pkg/storage/unified/resource/datastore_test.go b/pkg/storage/unified/resource/datastore_test.go index 1bdb97fb949..9b405847a9a 100644 --- a/pkg/storage/unified/resource/datastore_test.go +++ b/pkg/storage/unified/resource/datastore_test.go @@ -3,6 +3,7 @@ package resource import ( "bytes" "context" + "errors" "fmt" "io" "testing" @@ -86,6 +87,7 @@ func TestDataKey_Validate(t *testing.T) { key DataKey expectError bool errorMsg string + errorField string }{ { name: "valid key with created action", @@ -99,6 +101,18 @@ func TestDataKey_Validate(t *testing.T) { }, expectError: false, }, + { + name: "valid - underscore in namespace", + key: DataKey{ + Namespace: "test_namespace", + Group: "test-group", + Resource: "test-resource", + Name: "test-name", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: false, + }, { name: "valid key with updated action", key: DataKey{ @@ -124,23 +138,23 @@ func TestDataKey_Validate(t *testing.T) { expectError: false, }, { - name: "valid key with dots and dashes", + name: "valid - name ends with dash", key: DataKey{ - Namespace: "test.namespace-with-dashes", - Group: "test.group-123", - Resource: "test-resource.v1", - Name: "test-name.with.dots", + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "test-name-", ResourceVersion: rv, Action: DataActionCreated, }, expectError: false, }, { - name: "valid key with single character names", + name: "valid key with minimum character lengths", key: DataKey{ - Namespace: "a", - Group: "b", - Resource: "c", + Namespace: "abc", + Group: "bcd", + Resource: "cde", Name: "d", ResourceVersion: rv, Action: DataActionCreated, @@ -159,6 +173,54 @@ func TestDataKey_Validate(t *testing.T) { }, expectError: false, }, + { + name: "valid - uppercase in name", + key: DataKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "Test-Name", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: false, + }, + { + name: "valid - uppercase in namespace", + key: DataKey{ + Namespace: "Test-Namespace", + Group: "test-group", + Resource: "test-resource", + Name: "test-name", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: false, + }, + { + name: "valid - uppercase in group", + key: DataKey{ + Namespace: "test-namespace", + Group: "Test-Group", + Resource: "test-resource", + Name: "test-name", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: false, + }, + { + name: "valid - uppercase in resource", + key: DataKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "Test-Resource", + Name: "test-name", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: false, + }, // Invalid cases - empty fields { name: "invalid - empty namespace", @@ -171,7 +233,7 @@ func TestDataKey_Validate(t *testing.T) { Action: DataActionCreated, }, expectError: true, - errorMsg: "namespace is required", + errorMsg: ErrNamespaceRequired, }, { name: "invalid - empty group", @@ -184,7 +246,7 @@ func TestDataKey_Validate(t *testing.T) { Action: DataActionCreated, }, expectError: true, - errorMsg: "group is required", + errorField: "group", }, { name: "invalid - empty resource", @@ -197,7 +259,7 @@ func TestDataKey_Validate(t *testing.T) { Action: DataActionCreated, }, expectError: true, - errorMsg: "resource is required", + errorField: "resource", }, { name: "invalid - empty name", @@ -210,7 +272,7 @@ func TestDataKey_Validate(t *testing.T) { Action: DataActionCreated, }, expectError: true, - errorMsg: "name is required", + errorField: "name", }, { name: "invalid - empty action", @@ -223,7 +285,7 @@ func TestDataKey_Validate(t *testing.T) { Action: "", }, expectError: true, - errorMsg: "action is required", + errorMsg: ErrActionRequired, }, { name: "invalid - all fields empty", @@ -236,74 +298,21 @@ func TestDataKey_Validate(t *testing.T) { Action: "", }, expectError: true, - errorMsg: "group is required", - }, - // Invalid cases - uppercase characters - { - name: "invalid - uppercase in namespace", - key: DataKey{ - Namespace: "Test-Namespace", - Group: "test-group", - Resource: "test-resource", - Name: "test-name", - ResourceVersion: rv, - Action: DataActionCreated, - }, - expectError: true, - errorMsg: "namespace 'Test-Namespace' is invalid", - }, - { - name: "invalid - uppercase in group", - key: DataKey{ - Namespace: "test-namespace", - Group: "Test-Group", - Resource: "test-resource", - Name: "test-name", - ResourceVersion: rv, - Action: DataActionCreated, - }, - expectError: true, - errorMsg: "group 'Test-Group' is invalid", - }, - { - name: "invalid - uppercase in resource", - key: DataKey{ - Namespace: "test-namespace", - Group: "test-group", - Resource: "Test-Resource", - Name: "test-name", - ResourceVersion: rv, - Action: DataActionCreated, - }, - expectError: true, - errorMsg: "resource 'Test-Resource' is invalid", - }, - { - name: "invalid - uppercase in name", - key: DataKey{ - Namespace: "test-namespace", - Group: "test-group", - Resource: "test-resource", - Name: "Test-Name", - ResourceVersion: rv, - Action: DataActionCreated, - }, - expectError: true, - errorMsg: "name 'Test-Name' is invalid", + errorField: "namespace", }, // Invalid cases - invalid characters { - name: "invalid - underscore in namespace", + name: "invalid - key with dots and dashes", key: DataKey{ - Namespace: "test_namespace", - Group: "test-group", - Resource: "test-resource", - Name: "test-name", + Namespace: "test.namespace-with-dashes", + Group: "test.group-123", + Resource: "test-resource.v1", + Name: "test-name.with.dots", ResourceVersion: rv, Action: DataActionCreated, }, expectError: true, - errorMsg: "namespace 'test_namespace' is invalid", + errorField: "namespace", }, { name: "invalid - space in group", @@ -331,8 +340,154 @@ func TestDataKey_Validate(t *testing.T) { expectError: true, errorMsg: "resource 'test@resource' is invalid", }, + // Name validation tests - K8s qualified name format { - name: "invalid - slash in name", + name: "valid - K8s format with underscores", + key: DataKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "test_name_with_underscores", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: false, + }, + { + name: "valid - K8s format with dots", + key: DataKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "test.name.with.dots", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: false, + }, + { + name: "valid - K8s format mixed case", + key: DataKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "TestName123", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: false, + }, + { + name: "valid - Legacy Grafana shortid format", + key: DataKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "a1B2c3D4e5F6g7H8", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: false, + }, + { + name: "valid - Legacy format with dashes and underscores", + key: DataKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "test-name_with-mixed_chars123", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: false, + }, + { + name: "valid - Single character name", + key: DataKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "a", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: false, + }, + { + name: "valid - name starts with dash (legacy format)", + key: DataKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "-test-name", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: false, + }, + { + name: "valid - name ends with dash (legacy format)", + key: DataKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "test-name-", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: false, + }, + { + name: "valid - name starts with dot", + key: DataKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: ".test-name", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: false, + }, + { + name: "valid - name ends with dot", + key: DataKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "test-name.", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: false, + }, + { + name: "valid - name starts with underscore (legacy format)", + key: DataKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "_test-name", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: false, + }, + { + name: "valid - name ends with underscore (legacy format)", + key: DataKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "test-name_", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: false, + }, + // Invalid name cases + { + name: "invalid - name with slash", key: DataKey{ Namespace: "test-namespace", Group: "test-group", @@ -342,7 +497,46 @@ func TestDataKey_Validate(t *testing.T) { Action: DataActionCreated, }, expectError: true, - errorMsg: "name 'test/name' is invalid", + errorField: "name", + }, + { + name: "invalid - name with spaces", + key: DataKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "test name", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: true, + errorField: "name", + }, + { + name: "invalid - name with special characters", + key: DataKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "test@name#with$special", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: true, + errorField: "name", + }, + { + name: "invalid - empty name", + key: DataKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "", + ResourceVersion: rv, + Action: DataActionCreated, + }, + expectError: true, + errorField: "name", }, // Invalid cases - start/end with invalid characters { @@ -384,19 +578,6 @@ func TestDataKey_Validate(t *testing.T) { expectError: true, errorMsg: "resource '.test-resource' is invalid", }, - { - name: "invalid - name ends with dash", - key: DataKey{ - Namespace: "test-namespace", - Group: "test-group", - Resource: "test-resource", - Name: "test-name-", - ResourceVersion: rv, - Action: DataActionCreated, - }, - expectError: true, - errorMsg: "name 'test-name-' is invalid", - }, // Invalid cases - invalid action { name: "invalid - unknown action", @@ -421,6 +602,10 @@ func TestDataKey_Validate(t *testing.T) { if tt.errorMsg != "" { require.Contains(t, err.Error(), tt.errorMsg) } + var validationErr *ValidationError + if errors.Is(err, validationErr) && tt.errorField != "" { + require.Equal(t, tt.errorField, validationErr.Field) + } } else { require.NoError(t, err) } @@ -974,7 +1159,7 @@ func TestDataStore_ValidationEnforced(t *testing.T) { // Create an invalid key invalidKey := DataKey{ - Namespace: "Invalid-Namespace", // uppercase is invalid + Namespace: "Invalid-Namespace-$$$", Group: "test-group", Resource: "test-resource", Name: "test-name", @@ -988,21 +1173,27 @@ func TestDataStore_ValidationEnforced(t *testing.T) { _, err := ds.Get(ctx, invalidKey) require.Error(t, err) require.Contains(t, err.Error(), "invalid data key") - require.Contains(t, err.Error(), "namespace 'Invalid-Namespace' is invalid") + var validationErr ValidationError + require.True(t, errors.As(err, &validationErr)) + require.Equal(t, "namespace", validationErr.Field) }) t.Run("Save with invalid key returns validation error", func(t *testing.T) { err := ds.Save(ctx, invalidKey, testValue) require.Error(t, err) require.Contains(t, err.Error(), "invalid data key") - require.Contains(t, err.Error(), "namespace 'Invalid-Namespace' is invalid") + var validationErr ValidationError + require.True(t, errors.As(err, &validationErr)) + require.Equal(t, "namespace", validationErr.Field) }) t.Run("Delete with invalid key returns validation error", func(t *testing.T) { err := ds.Delete(ctx, invalidKey) require.Error(t, err) require.Contains(t, err.Error(), "invalid data key") - require.Contains(t, err.Error(), "namespace 'Invalid-Namespace' is invalid") + var validationErr ValidationError + require.True(t, errors.As(err, &validationErr)) + require.Equal(t, "namespace", validationErr.Field) }) // Test another type of invalid key @@ -1043,6 +1234,7 @@ func TestListRequestKey_Validate(t *testing.T) { key ListRequestKey expectError bool errorMsg string + errorField string }{ { name: "valid - all fields provided", @@ -1054,6 +1246,45 @@ func TestListRequestKey_Validate(t *testing.T) { }, expectError: false, }, + { + name: "valid - uppercase in namespace", + key: ListRequestKey{ + Namespace: "Test-Namespace", + Group: "test-group", + Resource: "test-resource", + Name: "test-name", + }, + expectError: false, + }, + { + name: "valid - uppercase in group and resource", + key: ListRequestKey{ + Namespace: "test-namespace", + Group: "Test-Group", + Resource: "test-resource", + Name: "test-name", + }, + expectError: false, + }, + { + name: "valid - uppercase in resource", + key: ListRequestKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "Test-Resource", + }, + expectError: false, + }, + { + name: "valid - underscore in namespace", + key: ListRequestKey{ + Namespace: "test_namespace", + Group: "test-group", + Resource: "test-resource", + Name: "test-name", + }, + expectError: false, + }, { name: "valid - only group and resource", key: ListRequestKey{ @@ -1075,7 +1306,47 @@ func TestListRequestKey_Validate(t *testing.T) { name: "invalid - all empty", key: ListRequestKey{}, expectError: true, - errorMsg: "group is required", + errorField: "namespace", + }, + { + name: "valid - legacy grafana uid 1", + key: ListRequestKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "_4OV_5Nmz", + }, + expectError: false, + }, + { + name: "valid - legacy grafana uid 2", + key: ListRequestKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "-Y-tnEDWk", + }, + expectError: false, + }, + { + name: "valid - legacy grafana uid 3", + key: ListRequestKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "000000005", + }, + expectError: false, + }, + { + name: "valid - uppercase in name", + key: ListRequestKey{ + Namespace: "test-namespace", + Group: "test-group", + Resource: "test-resource", + Name: "Test-Name", + }, + expectError: false, }, // Invalid hierarchical cases { @@ -1084,7 +1355,7 @@ func TestListRequestKey_Validate(t *testing.T) { Group: "test-group", }, expectError: true, - errorMsg: "resource is required", + errorField: "resource", }, { name: "invalid - name without namespace", @@ -1094,7 +1365,7 @@ func TestListRequestKey_Validate(t *testing.T) { Group: "test-group", }, expectError: true, - errorMsg: "name must be empty when namespace is empty", + errorMsg: ErrNameMustBeEmptyWhenNamespaceEmpty, }, { name: "invalid - name without group and resource", @@ -1103,63 +1374,9 @@ func TestListRequestKey_Validate(t *testing.T) { Name: "test-name", }, expectError: true, - errorMsg: "group is required", + errorField: "group", }, // Invalid naming cases - { - name: "invalid - uppercase in namespace", - key: ListRequestKey{ - Namespace: "Test-Namespace", - Group: "test-group", - Resource: "test-resource", - Name: "test-name", - }, - expectError: true, - errorMsg: "namespace 'Test-Namespace' is invalid", - }, - { - name: "invalid - uppercase in group and resource", - key: ListRequestKey{ - Namespace: "test-namespace", - Group: "Test-Group", - Resource: "test-resource", - Name: "test-name", - }, - expectError: true, - errorMsg: "group 'Test-Group' is invalid", - }, - { - name: "invalid - uppercase in resource", - key: ListRequestKey{ - Namespace: "test-namespace", - Group: "test-group", - Resource: "Test-Resource", - }, - expectError: true, - errorMsg: "resource 'Test-Resource' is invalid", - }, - { - name: "invalid - uppercase in name", - key: ListRequestKey{ - Namespace: "test-namespace", - Group: "test-group", - Resource: "test-resource", - Name: "Test-Name", - }, - expectError: true, - errorMsg: "name 'Test-Name' is invalid", - }, - { - name: "invalid - underscore in namespace", - key: ListRequestKey{ - Namespace: "test_namespace", - Group: "test-group", - Resource: "test-resource", - Name: "test-name", - }, - expectError: true, - errorMsg: "namespace 'test_namespace' is invalid", - }, { name: "invalid - starts with dash", key: ListRequestKey{ @@ -1182,6 +1399,16 @@ func TestListRequestKey_Validate(t *testing.T) { expectError: true, errorMsg: "group 'test-group.' is invalid", }, + { + name: "invalid - name contains invalid char", + key: ListRequestKey{ + Namespace: "test-namespace", + Group: "test-group.", + Resource: "test-resource", + Name: "test$name", + }, + expectError: true, + }, } for _, tt := range tests { @@ -2288,10 +2515,11 @@ func TestDataKey_SameResource(t *testing.T) { func TestGetRequestKey_Validate(t *testing.T) { tests := []struct { - name string - key GetRequestKey - expectErr bool - wantError string + name string + key GetRequestKey + expectErr bool + wantError string + errorField string }{ { name: "valid key", @@ -2313,6 +2541,16 @@ func TestGetRequestKey_Validate(t *testing.T) { }, expectErr: false, }, + { + name: "valid grafana name - ends with dot", + key: GetRequestKey{ + Group: "apps", + Resource: "resources", + Namespace: "default", + Name: ".123_hello", + }, + expectErr: false, + }, { name: "missing group", key: GetRequestKey{ @@ -2320,8 +2558,8 @@ func TestGetRequestKey_Validate(t *testing.T) { Namespace: "default", Name: "test-resource", }, - expectErr: true, - wantError: "group is required", + expectErr: true, + errorField: "group", }, { name: "missing resource", @@ -2330,8 +2568,8 @@ func TestGetRequestKey_Validate(t *testing.T) { Namespace: "default", Name: "test-resource", }, - expectErr: true, - wantError: "resource is required", + expectErr: true, + errorField: "resource", }, { name: "missing namespace", @@ -2340,8 +2578,8 @@ func TestGetRequestKey_Validate(t *testing.T) { Resource: "resources", Name: "test-resource", }, - expectErr: true, - wantError: "namespace is required", + expectErr: true, + errorField: "namespace", }, { name: "missing name", @@ -2350,30 +2588,19 @@ func TestGetRequestKey_Validate(t *testing.T) { Resource: "resources", Namespace: "default", }, - expectErr: true, - wantError: "name is required", + expectErr: true, + errorField: "name", }, { - name: "invalid namespace - uppercase", + name: "invalid group - underscore at start", key: GetRequestKey{ - Group: "apps", - Resource: "resources", - Namespace: "Default", - Name: "test-resource", - }, - expectErr: true, - wantError: "namespace 'Default' is invalid", - }, - { - name: "invalid group - underscore", - key: GetRequestKey{ - Group: "apps_v1", + Group: "_apps_v1", Resource: "resources", Namespace: "default", Name: "test-resource", }, - expectErr: true, - wantError: "group 'apps_v1' is invalid", + expectErr: true, + errorField: "group", }, { name: "invalid resource - starts with dash", @@ -2383,19 +2610,8 @@ func TestGetRequestKey_Validate(t *testing.T) { Namespace: "default", Name: "test-resource", }, - expectErr: true, - wantError: "resource '-resources' is invalid", - }, - { - name: "invalid name - ends with dot", - key: GetRequestKey{ - Group: "apps", - Resource: "resources", - Namespace: "default", - Name: "test-resource.", - }, - expectErr: true, - wantError: "name 'test-resource.' is invalid", + expectErr: true, + errorField: "resource", }, } @@ -2407,6 +2623,10 @@ func TestGetRequestKey_Validate(t *testing.T) { if tt.wantError != "" { require.Contains(t, err.Error(), tt.wantError) } + var validationErr *ValidationError + if errors.Is(err, validationErr) && tt.errorField != "" { + require.Equal(t, tt.errorField, validationErr.Field) + } } else { require.NoError(t, err) } diff --git a/pkg/storage/unified/resource/errors.go b/pkg/storage/unified/resource/errors.go index a0402be1dcb..91b1332c684 100644 --- a/pkg/storage/unified/resource/errors.go +++ b/pkg/storage/unified/resource/errors.go @@ -2,6 +2,7 @@ package resource import ( "errors" + "fmt" "net/http" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" @@ -199,3 +200,25 @@ func HandleQueueError[T any](err error, makeResp func(*resourcepb.ErrorResult) * } return makeResp(AsErrorResult(err)), nil } + +var ( + ErrNamespaceRequired = "namespace is required" + ErrResourceVersionInvalid = "resource version must be positive" + ErrActionRequired = "action is required" + ErrActionInvalid = "action is invalid: must be one of 'created', 'updated', or 'deleted'" + ErrNameMustBeEmptyWhenNamespaceEmpty = "name must be empty when namespace is empty" +) + +type ValidationError struct { + Field string + Value string + Msg string +} + +func (e ValidationError) Error() string { + return fmt.Sprintf("%s '%s' is invalid: %s", e.Field, e.Value, e.Msg) +} + +func NewValidationError(field, value, msg string) error { + return ValidationError{Field: field, Value: value, Msg: msg} +} diff --git a/pkg/storage/unified/resource/eventstore.go b/pkg/storage/unified/resource/eventstore.go index 6558a81839c..543849efe9f 100644 --- a/pkg/storage/unified/resource/eventstore.go +++ b/pkg/storage/unified/resource/eventstore.go @@ -3,6 +3,7 @@ package resource import ( "context" "encoding/json" + "errors" "fmt" "iter" "strconv" @@ -10,6 +11,7 @@ import ( "time" "github.com/bwmarrin/snowflake" + "github.com/grafana/grafana/pkg/apimachinery/validation" ) const ( @@ -37,46 +39,38 @@ func (k EventKey) String() string { func (k EventKey) Validate() error { if k.Namespace == "" { - return fmt.Errorf("namespace cannot be empty") - } - if k.Group == "" { - return fmt.Errorf("group cannot be empty") - } - if k.Resource == "" { - return fmt.Errorf("resource cannot be empty") - } - if k.Name == "" { - return fmt.Errorf("name cannot be empty") + return NewValidationError("namespace", k.Namespace, ErrNamespaceRequired) } if k.ResourceVersion < 0 { - return fmt.Errorf("resource version must be non-negative") + return errors.New(ErrResourceVersionInvalid) } if k.Action == "" { - return fmt.Errorf("action cannot be empty") + return NewValidationError("action", string(k.Action), ErrActionRequired) } - if k.Folder != "" && !validNameRegex.MatchString(k.Folder) { - return fmt.Errorf("folder '%s' is invalid", k.Folder) + + // Validate each field against the naming rules + // Validate naming conventions for all required fields + if err := validation.IsValidNamespace(k.Namespace); err != nil { + return NewValidationError("namespace", k.Namespace, err[0]) } - // Validate each field against the naming rules (reusing the regex from datastore.go) - if !validNameRegex.MatchString(k.Namespace) { - return fmt.Errorf("namespace '%s' is invalid", k.Namespace) + if err := validation.IsValidGroup(k.Group); err != nil { + return NewValidationError("group", k.Group, err[0]) } - if !validNameRegex.MatchString(k.Group) { - return fmt.Errorf("group '%s' is invalid", k.Group) + if err := validation.IsValidResource(k.Resource); err != nil { + return NewValidationError("resource", k.Resource, err[0]) } - if !validNameRegex.MatchString(k.Resource) { - return fmt.Errorf("resource '%s' is invalid", k.Resource) + if err := validation.IsValidGrafanaName(k.Name); err != nil { + return NewValidationError("name", k.Name, err[0]) } - if !validNameRegex.MatchString(k.Name) { - return fmt.Errorf("name '%s' is invalid", k.Name) - } - if k.Folder != "" && !validNameRegex.MatchString(k.Folder) { - return fmt.Errorf("folder '%s' is invalid", k.Folder) + if k.Folder != "" { + if err := validation.IsValidGrafanaName(k.Folder); err != nil { + return NewValidationError("folder", k.Folder, err[0]) + } } switch k.Action { case DataActionCreated, DataActionUpdated, DataActionDeleted: default: - return fmt.Errorf("action '%s' is invalid: must be one of 'created', 'updated', or 'deleted'", k.Action) + return NewValidationError("action", string(k.Action), ErrActionInvalid) } return nil @@ -148,6 +142,7 @@ func (n *eventStore) Save(ctx context.Context, event Event) error { Name: event.Name, ResourceVersion: event.ResourceVersion, Action: event.Action, + //TODO why isnt folder part of the key? } if err := eventKey.Validate(); err != nil { diff --git a/pkg/storage/unified/resource/keys.go b/pkg/storage/unified/resource/keys.go index 324a2128633..8c9b4b42a93 100644 --- a/pkg/storage/unified/resource/keys.go +++ b/pkg/storage/unified/resource/keys.go @@ -41,7 +41,7 @@ func verifyRequestKeyNamespaceGroupResource(key *resourcepb.ResourceKey) *resour if err := validation.IsValidGroup(key.Group); err != nil { return NewBadRequestError(err[0]) } - if err := validation.IsValidateResource(key.Resource); err != nil { + if err := validation.IsValidResource(key.Resource); err != nil { return NewBadRequestError(err[0]) } return nil diff --git a/pkg/storage/unified/resource/kv.go b/pkg/storage/unified/resource/kv.go index 613841c5994..a4215a1570a 100644 --- a/pkg/storage/unified/resource/kv.go +++ b/pkg/storage/unified/resource/kv.go @@ -259,9 +259,9 @@ func PrefixRangeEnd(prefix string) string { var ( // validKeyRegex validates keys used in the unified storage - // Keys can contain lowercase alphanumeric characters, '-', '.', '/', and '~' + // Keys can contain alphanumeric characters (both upper and lowercase), '-', '.', '/', and '~' // Any combination of these characters is allowed as long as the key is not empty - validKeyRegex = regexp.MustCompile(`^[a-z0-9./~-]+$`) + validKeyRegex = regexp.MustCompile(`^[a-zA-Z0-9./~_-]+$`) ) func IsValidKey(key string) bool { diff --git a/pkg/storage/unified/resource/kv_test.go b/pkg/storage/unified/resource/kv_test.go index 27f24e19122..b5625405b73 100644 --- a/pkg/storage/unified/resource/kv_test.go +++ b/pkg/storage/unified/resource/kv_test.go @@ -235,17 +235,19 @@ func TestIsValidKey(t *testing.T) { {"data key format", "ns/group/resource/name/123~created", true}, {"metadata key format", "group/resource/ns/name/123~created~folder", true}, {"metadata key format ending with a ~", "group/resource/ns/name/123~created~", true}, + {"uppercase letters", "Valid", true}, + {"underscores", "a_b", true}, + {"key with underscores and mixed chars", "4_D6mSh4z", true}, + {"complex key with underscores", "ns/group_name/resource-name/Name_123~action-type", true}, // invalid keys {"empty key", "", false}, - {"uppercase letters", "Invalid", false}, {"special characters", "a@b", false}, {"spaces", "a b", false}, {"leading space", " key", false}, {"trailing space", "key ", false}, {"tab character", "a\tb", false}, {"newline character", "a\nb", false}, - {"underscores", "a_b", false}, } for _, tt := range tests { diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 1a304d49f68..59ae4e1dc83 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -554,7 +554,7 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *resour return nil, NewBadRequestError( fmt.Sprintf("key/name do not match (key: %s, name: %s)", key.Name, obj.GetName())) } - if errs := validation.IsValidGrafanaName(obj.GetName()); err != nil { + if errs := validation.IsValidGrafanaName(obj.GetName()); errs != nil { return nil, NewBadRequestError(errs[0]) } From d291671ed1770ef0c2f7ee2e97247f1ff64f5063 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Thu, 9 Oct 2025 20:30:38 +0200 Subject: [PATCH 105/578] Chore: Update codeowners and project ids for pyroscope (#112222) * update codeowners and project ids for pyroscope * remove duplicates --- .github/CODEOWNERS | 14 +++++++------- .github/commands.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 114a4d8feed..b9daf7a2487 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -302,7 +302,7 @@ /devenv/docker/blocks/prometheus_random_data/ @grafana/oss-big-tent /devenv/docker/blocks/prometheus_high_card/ @grafana/oss-big-tent /devenv/docker/blocks/prometheus_utf8/ @grafana/oss-big-tent -/devenv/docker/blocks/pyroscope/ @grafana/observability-traces-and-profiling +/devenv/docker/blocks/pyroscope/ @grafana/oss-big-tent /devenv/docker/blocks/redis/ @bergquist /devenv/docker/blocks/sensugo/ @grafana/grafana-backend-group /devenv/docker/blocks/slow_proxy/ @bergquist @@ -352,8 +352,8 @@ /pkg/tsdb/prometheus/ @grafana/oss-big-tent /pkg/tsdb/elasticsearch/ @grafana/partner-datasources /pkg/tsdb/loki/ @grafana/oss-big-tent -/pkg/tsdb/tempo/ @grafana/oss-big-tent @grafana/observability-traces-and-profiling -/pkg/tsdb/grafana-pyroscope-datasource/ @grafana/observability-traces-and-profiling +/pkg/tsdb/tempo/ @grafana/oss-big-tent +/pkg/tsdb/grafana-pyroscope-datasource/ @grafana/oss-big-tent /pkg/tsdb/parca/ @grafana/oss-big-tent # OSS Big Tent backend code @@ -610,7 +610,7 @@ /packages/grafana-o11y-ds-frontend/ @grafana/observability-logs /packages/grafana-o11y-ds-frontend/src/IntervalInput/ @grafana/observability-traces-and-profiling /packages/grafana-o11y-ds-frontend/src/NodeGraph/ @grafana/observability-traces-and-profiling -/packages/grafana-o11y-ds-frontend/src/pyroscope/ @grafana/observability-traces-and-profiling +/packages/grafana-o11y-ds-frontend/src/pyroscope/ @grafana/oss-big-tent @grafana/observability-traces-and-profiling /packages/grafana-o11y-ds-frontend/src/SpanBar/ @grafana/oss-big-tent @grafana/observability-traces-and-profiling /packages/grafana-o11y-ds-frontend/src/TraceToLogs/ @grafana/oss-big-tent @grafana/observability-traces-and-profiling /packages/grafana-o11y-ds-frontend/src/TraceToMetrics/ @grafana/oss-big-tent @grafana/observability-traces-and-profiling @@ -686,7 +686,7 @@ /packages/grafana-schema/src/**/gauge @grafana/dataviz-squad /packages/grafana-schema/src/**/geomap @grafana/dataviz-squad /packages/grafana-schema/src/**/googlecloudmonitoring @grafana/partner-datasources -/packages/grafana-schema/src/**/grafanapyroscope @grafana/observability-traces-and-profiling +/packages/grafana-schema/src/**/grafanapyroscope @grafana/oss-big-tent /packages/grafana-schema/src/**/heatmap @grafana/dataviz-squad /packages/grafana-schema/src/**/histogram @grafana/dataviz-squad /packages/grafana-schema/src/**/logs @grafana/observability-logs @@ -1090,8 +1090,8 @@ eslint-suppressions.json @grafanabot /public/app/plugins/datasource/prometheus/ @grafana/oss-big-tent /public/app/plugins/datasource/cloud-monitoring/ @grafana/partner-datasources /public/app/plugins/datasource/zipkin/ @grafana/oss-big-tent -/public/app/plugins/datasource/tempo/ @grafana/oss-big-tent @grafana/observability-traces-and-profiling -/public/app/plugins/datasource/grafana-pyroscope-datasource/ @grafana/observability-traces-and-profiling +/public/app/plugins/datasource/tempo/ @grafana/oss-big-tent +/public/app/plugins/datasource/grafana-pyroscope-datasource/ @grafana/oss-big-tent /public/app/plugins/datasource/parca/ @grafana/oss-big-tent /public/app/plugins/datasource/alertmanager/ @grafana/alerting-squad diff --git a/.github/commands.json b/.github/commands.json index 7b9c64b18e6..29106576726 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -144,7 +144,7 @@ "name": "datasource/grafana-pyroscope", "action": "addToProject", "addToProject": { - "url": "https://github.com/orgs/grafana/projects/221" + "url": "https://github.com/orgs/grafana/projects/457" } }, { From e6e58c3a56a8c77eb4ba3222690bdc49a3ec9ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Thu, 9 Oct 2025 21:17:50 +0200 Subject: [PATCH 106/578] fix: implement ColumnCheckSQL to make AddColumn idempotent (#112227) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SQLite dialect now checks column existence via PRAGMA table_info, enabling IfColumnNotExistsCondition to work correctly. Previously, BaseDialect returned empty SQL, so AddColumn ran unconditionally and could fail with “duplicate column name” under parallel CI runs. - Prevents duplicate-column errors in SQLite migrations (e.g. unified storage adding previous_resource_version) when migration locking/logging don’t serialize execution. - No change for other dialects. --- pkg/services/sqlstore/migrator/sqlite_dialect.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/services/sqlstore/migrator/sqlite_dialect.go b/pkg/services/sqlstore/migrator/sqlite_dialect.go index d4302c18181..921ef9696c4 100644 --- a/pkg/services/sqlstore/migrator/sqlite_dialect.go +++ b/pkg/services/sqlstore/migrator/sqlite_dialect.go @@ -87,6 +87,17 @@ func (db *SQLite3) IndexCheckSQL(tableName, indexName string) (string, []any) { return sql, args } +func (db *SQLite3) ColumnCheckSQL(tableName, columnName string) (string, []any) { + // Use PRAGMA table_info to check if a column exists on a table. In SQLite, quoting with backticks inside + // pragma_table_info() can be interpreted as an identifier/column. Instead, pass the table name as a + // string literal to avoid ambiguity. We cannot parameterize identifiers, but pragma_table_info accepts string + // literals, so we embed a single-quoted literal safely by replacing single quotes if any. + // Note: tableName is expected to be a trusted identifier from migrations. + safeTable := strings.ReplaceAll(tableName, "'", "''") + sql := "SELECT 1 FROM pragma_table_info('" + safeTable + "') WHERE name = ?" + return sql, []any{columnName} +} + func (db *SQLite3) DropIndexSQL(tableName string, index *Index) string { quote := db.Quote // var unique string From 6af50482a1c26ff96c08a6f40f7059c5bf70e0f4 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 9 Oct 2025 22:22:59 +0300 Subject: [PATCH 107/578] Chore: CleanupTestDB in unifed storage tests (#112223) --- pkg/storage/unified/sql/test/integration_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/storage/unified/sql/test/integration_test.go b/pkg/storage/unified/sql/test/integration_test.go index 347a28c4f11..fe0c5b049fe 100644 --- a/pkg/storage/unified/sql/test/integration_test.go +++ b/pkg/storage/unified/sql/test/integration_test.go @@ -69,6 +69,7 @@ func TestMain(m *testing.M) { func TestIntegrationStorageServer(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) + t.Cleanup(db.CleanupTestDB) unitest.RunStorageServerTest(t, func(ctx context.Context) resource.StorageBackend { return newTestBackend(t, true, 0) @@ -78,6 +79,7 @@ func TestIntegrationStorageServer(t *testing.T) { // TestStorageBackend is a test for the StorageBackend interface. func TestIntegrationSQLStorageBackend(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) + t.Cleanup(db.CleanupTestDB) t.Run("IsHA (polling notifier)", func(t *testing.T) { unitest.RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend { From 5c815a1733a5c38533b9ed25584efa196cf1bd46 Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:29:06 -0600 Subject: [PATCH 108/578] Dashboard Migrations: v12 - template variables (#110253) * migrate to v19 * migrate to v18 * Migration to be verified: v17 Convert minSpan to maxPerRow in panels * Migration to be verified: 16 Grid layout migration * Refactor v17 and v19 migrations to use shared helper functions * Migration to be verified: 15 No-op migration for schema consistency * Migration to be verified: 14 Shared crosshair to graph tooltip migration * cleanup * wip * complete migration * fix lint issues * refactor and test with minimal graph config * update tests * migrate to v12 * extract defaults outside the func * lint * lint * add missing showValues prop * update * add context and fix latest version * add context * generate snapshots * v13 should be no-op * clean up * fix tests * fix test * remove v28 * remove singlestat migraiton from frontend migrator because this is an automigration * remove unused function * Remove v24 table plugin logic * cleanup * remove plugin version for automigrate as it was used only in v24 and v28 that have been removed * cleanup * es int --------- Co-authored-by: Dominik Prokop --- .../pkg/migration/frontend_defaults.go | 12 +- .../pkg/migration/schemaversion/migrations.go | 3 +- .../pkg/migration/schemaversion/v12.go | 66 ++++ .../pkg/migration/schemaversion/v12_test.go | 316 ++++++++++++++++++ .../pkg/migration/schemaversion/v36.go | 3 +- .../input/v12.template-variables.json | 38 +++ .../v12.template-variables.v42.json | 81 +++++ .../v12.template-variables.v12.json | 75 +++++ eslint-suppressions.json | 2 +- .../dashboard/state/DashboardMigrator.ts | 51 --- 10 files changed, 589 insertions(+), 58 deletions(-) create mode 100644 apps/dashboard/pkg/migration/schemaversion/v12.go create mode 100644 apps/dashboard/pkg/migration/schemaversion/v12_test.go create mode 100644 apps/dashboard/pkg/migration/testdata/input/v12.template-variables.json create mode 100644 apps/dashboard/pkg/migration/testdata/output/latest_version/v12.template-variables.v42.json create mode 100644 apps/dashboard/pkg/migration/testdata/output/single_version/v12.template-variables.v12.json diff --git a/apps/dashboard/pkg/migration/frontend_defaults.go b/apps/dashboard/pkg/migration/frontend_defaults.go index 76914a1b03a..97541d1f8de 100644 --- a/apps/dashboard/pkg/migration/frontend_defaults.go +++ b/apps/dashboard/pkg/migration/frontend_defaults.go @@ -2,6 +2,8 @@ package migration import ( "sort" + + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" ) // applyFrontendDefaults applies all DashboardModel constructor defaults @@ -793,10 +795,12 @@ func cleanupVariable(variable map[string]interface{}) { if variableType, ok := variable["type"].(string); ok { switch variableType { case "query": - // Query variables: keep options: [] if refresh !== never - // Since refresh is not specified in the input, it defaults to not "never" - if _, hasOptions := variable["options"]; !hasOptions { - variable["options"] = []interface{}{} + // Query variables: keep options: [] if refresh !== never (matches frontend getSaveModel logic) + refresh := schemaversion.GetIntValue(variable, "refresh", 1) // Default to 1 (onDashboardLoad) if not specified + if refresh != 0 { // 0 = VariableRefreshNever + if _, hasOptions := variable["options"]; !hasOptions { + variable["options"] = []interface{}{} + } } case "constant": // Constant variables: remove options completely diff --git a/apps/dashboard/pkg/migration/schemaversion/migrations.go b/apps/dashboard/pkg/migration/schemaversion/migrations.go index df8d1030ffb..86ad0f8a631 100644 --- a/apps/dashboard/pkg/migration/schemaversion/migrations.go +++ b/apps/dashboard/pkg/migration/schemaversion/migrations.go @@ -7,7 +7,7 @@ import ( ) const ( - MIN_VERSION = 12 + MIN_VERSION = 11 LATEST_VERSION = 42 ) @@ -35,6 +35,7 @@ type PanelPluginInfo struct { func GetMigrations(dsInfoProvider DataSourceInfoProvider) map[int]SchemaVersionMigrationFunc { return map[int]SchemaVersionMigrationFunc{ + 12: V12, 13: V13, 14: V14, 15: V15, diff --git a/apps/dashboard/pkg/migration/schemaversion/v12.go b/apps/dashboard/pkg/migration/schemaversion/v12.go new file mode 100644 index 00000000000..2eb97281937 --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v12.go @@ -0,0 +1,66 @@ +package schemaversion + +import "context" + +// V12 migrates template variables to update their refresh and hide properties. +// This migration ensures that: +// 1. Variables with refresh=true get refresh=1, and variables with refresh=false get refresh=0 +// 2. Variables with hideVariable=true get hide=2 (hide variable) +// 3. Variables with hideLabel=true get hide=1 (hide label) +// +// Example before migration: +// +// "templating": { +// "list": [ +// { "type": "query", "name": "var1", "refresh": true, "hideVariable": true }, +// { "type": "query", "name": "var2", "refresh": false, "hideLabel": true } +// ] +// } +// +// Example after migration: +// +// "templating": { +// "list": [ +// { "type": "query", "name": "var1", "refresh": 1, "hide": 2 }, +// { "type": "query", "name": "var2", "refresh": 0, "hide": 1 } +// ] +// } +func V12(_ context.Context, dashboard map[string]interface{}) error { + dashboard["schemaVersion"] = 12 + + templating, ok := dashboard["templating"].(map[string]interface{}) + if !ok { + return nil + } + + list, ok := templating["list"].([]interface{}) + if !ok { + return nil + } + + for _, v := range list { + variable, ok := v.(map[string]interface{}) + if !ok { + continue + } + + // Update refresh property + if _, hasRefresh := variable["refresh"]; hasRefresh { + if GetBoolValue(variable, "refresh") { + variable["refresh"] = 1 + } else { + variable["refresh"] = 0 + } + } + + // Update hide property based on hideVariable and hideLabel + // hideVariable takes priority over hideLabel + if GetBoolValue(variable, "hideVariable") { + variable["hide"] = 2 + } else if GetBoolValue(variable, "hideLabel") { + variable["hide"] = 1 + } + } + + return nil +} diff --git a/apps/dashboard/pkg/migration/schemaversion/v12_test.go b/apps/dashboard/pkg/migration/schemaversion/v12_test.go new file mode 100644 index 00000000000..eef78ee450c --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v12_test.go @@ -0,0 +1,316 @@ +package schemaversion_test + +import ( + "testing" + + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" +) + +func TestV12(t *testing.T) { + tests := []migrationTestCase{ + { + name: "variable with refresh=true gets refresh=1", + input: map[string]interface{}{ + "title": "V12 Refresh True Test", + "schemaVersion": 11, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "refresh_true_var", + "refresh": true, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V12 Refresh True Test", + "schemaVersion": 12, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "refresh_true_var", + "refresh": 1, + }, + }, + }, + }, + }, + { + name: "variable with refresh=false gets refresh=0", + input: map[string]interface{}{ + "title": "V12 Refresh False Test", + "schemaVersion": 11, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "refresh_false_var", + "refresh": false, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V12 Refresh False Test", + "schemaVersion": 12, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "refresh_false_var", + "refresh": 0, + }, + }, + }, + }, + }, + { + name: "variable with hideVariable=true gets hide=2", + input: map[string]interface{}{ + "title": "V12 Hide Variable Test", + "schemaVersion": 11, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "hide_variable_var", + "hideVariable": true, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V12 Hide Variable Test", + "schemaVersion": 12, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "hide_variable_var", + "hideVariable": true, + "hide": 2, + }, + }, + }, + }, + }, + { + name: "variable with hideLabel=true gets hide=1", + input: map[string]interface{}{ + "title": "V12 Hide Label Test", + "schemaVersion": 11, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "hide_label_var", + "hideLabel": true, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V12 Hide Label Test", + "schemaVersion": 12, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "hide_label_var", + "hideLabel": true, + "hide": 1, + }, + }, + }, + }, + }, + { + name: "variable with both hideVariable and hideLabel prioritizes hideVariable", + input: map[string]interface{}{ + "title": "V12 Hide Priority Test", + "schemaVersion": 11, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "priority_var", + "hideVariable": true, + "hideLabel": true, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V12 Hide Priority Test", + "schemaVersion": 12, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "priority_var", + "hideVariable": true, + "hideLabel": true, + "hide": 2, + }, + }, + }, + }, + }, + { + name: "variable with no refresh or hide properties is unchanged", + input: map[string]interface{}{ + "title": "V12 No Properties Test", + "schemaVersion": 11, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "no_properties_var", + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V12 No Properties Test", + "schemaVersion": 12, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "no_properties_var", + }, + }, + }, + }, + }, + { + name: "dashboard without templating is unchanged", + input: map[string]interface{}{ + "title": "V12 No Templating Test", + "schemaVersion": 11, + }, + expected: map[string]interface{}{ + "title": "V12 No Templating Test", + "schemaVersion": 12, + }, + }, + { + name: "dashboard with empty templating list is unchanged", + input: map[string]interface{}{ + "title": "V12 Empty Templating Test", + "schemaVersion": 11, + "templating": map[string]interface{}{ + "list": []interface{}{}, + }, + }, + expected: map[string]interface{}{ + "title": "V12 Empty Templating Test", + "schemaVersion": 12, + "templating": map[string]interface{}{ + "list": []interface{}{}, + }, + }, + }, + { + name: "variables with hideVariable=false and hideLabel=false do not get hide property", + input: map[string]interface{}{ + "title": "V12 False Hide Properties Test", + "schemaVersion": 11, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "false_hide_var", + "hideVariable": false, + "hideLabel": false, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V12 False Hide Properties Test", + "schemaVersion": 12, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "false_hide_var", + "hideVariable": false, + "hideLabel": false, + }, + }, + }, + }, + }, + { + name: "variable with hideVariable=false but hideLabel=true gets hide=1", + input: map[string]interface{}{ + "title": "V12 Mixed Hide Properties Test", + "schemaVersion": 11, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "mixed_hide_var", + "hideVariable": false, + "hideLabel": true, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V12 Mixed Hide Properties Test", + "schemaVersion": 12, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "mixed_hide_var", + "hideVariable": false, + "hideLabel": true, + "hide": 1, + }, + }, + }, + }, + }, + { + name: "variable with all properties gets all migrations applied", + input: map[string]interface{}{ + "title": "V12 All Properties Test", + "schemaVersion": 11, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "all_properties_var", + "refresh": true, + "hideVariable": true, + "hideLabel": false, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V12 All Properties Test", + "schemaVersion": 12, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "type": "query", + "name": "all_properties_var", + "refresh": 1, + "hideVariable": true, + "hideLabel": false, + "hide": 2, + }, + }, + }, + }, + }, + } + + runMigrationTests(t, tests, schemaversion.V12) +} diff --git a/apps/dashboard/pkg/migration/schemaversion/v36.go b/apps/dashboard/pkg/migration/schemaversion/v36.go index c45c4ad56a2..7874e80b1d9 100644 --- a/apps/dashboard/pkg/migration/schemaversion/v36.go +++ b/apps/dashboard/pkg/migration/schemaversion/v36.go @@ -139,7 +139,8 @@ func migrateTemplateVariables(dashboard map[string]interface{}, datasources []Da ds, exists := varMap["datasource"] // Handle null datasource variables by setting to default (matches frontend behavior) - if !exists || ds == nil { + // Only add datasource if it's explicitly null, not if it doesn't exist + if exists && ds == nil { varMap["datasource"] = GetDataSourceRef(defaultDS) } // Note: Frontend v36 migration only converts null datasources to default objects diff --git a/apps/dashboard/pkg/migration/testdata/input/v12.template-variables.json b/apps/dashboard/pkg/migration/testdata/input/v12.template-variables.json new file mode 100644 index 00000000000..e851b6908a7 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/input/v12.template-variables.json @@ -0,0 +1,38 @@ +{ + "title": "V12 Template Variables Migration Test", + "schemaVersion": 11, + "templating": { + "list": [ + { + "type": "query", + "name": "refresh_true_var", + "refresh": true + }, + { + "type": "query", + "name": "refresh_false_var", + "refresh": false + }, + { + "type": "query", + "name": "hide_variable_var", + "hideVariable": true + }, + { + "type": "query", + "name": "hide_label_var", + "hideLabel": true + }, + { + "type": "query", + "name": "priority_var", + "hideVariable": true, + "hideLabel": true + }, + { + "type": "query", + "name": "no_properties_var" + } + ] + } + } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v12.template-variables.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v12.template-variables.v42.json new file mode 100644 index 00000000000..85b7de371d5 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v12.template-variables.v42.json @@ -0,0 +1,81 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "name": "refresh_true_var", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "name": "refresh_false_var", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "hide": 2, + "hideVariable": true, + "name": "hide_variable_var", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "hide": 1, + "hideLabel": true, + "name": "hide_label_var", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "hide": 2, + "hideLabel": true, + "hideVariable": true, + "name": "priority_var", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "name": "no_properties_var", + "options": [], + "refresh": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V12 Template Variables Migration Test", + "weekStart": "" +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v12.template-variables.v12.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v12.template-variables.v12.json new file mode 100644 index 00000000000..b1de134895f --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v12.template-variables.v12.json @@ -0,0 +1,75 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [], + "schemaVersion": 12, + "tags": [], + "templating": { + "list": [ + { + "name": "refresh_true_var", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "name": "refresh_false_var", + "refresh": 0, + "type": "query" + }, + { + "hide": 2, + "hideVariable": true, + "name": "hide_variable_var", + "options": [], + "type": "query" + }, + { + "hide": 1, + "hideLabel": true, + "name": "hide_label_var", + "options": [], + "type": "query" + }, + { + "hide": 2, + "hideLabel": true, + "hideVariable": true, + "name": "priority_var", + "options": [], + "type": "query" + }, + { + "name": "no_properties_var", + "options": [], + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V12 Template Variables Migration Test", + "weekStart": "" +} \ No newline at end of file diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 7e58c1ec3c3..1d3a1535b9b 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -2544,7 +2544,7 @@ "count": 2 }, "@typescript-eslint/no-explicit-any": { - "count": 20 + "count": 19 } }, "public/app/features/dashboard/state/DashboardModel.repeat.test.ts": { diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index c18fd24a6f8..e027ed16f1e 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -347,57 +347,6 @@ export class DashboardMigrator { }); } - if (oldVersion < 12 && finalTargetVersion >= 12) { - // update graph yaxes changes - panelUpgrades.push((panel: any) => { - if (panel.type !== 'graph') { - return panel; - } - if (!panel.grid) { - return panel; - } - - if (!panel.yaxes) { - panel.yaxes = [ - { - show: panel['y-axis'], - min: panel.grid.leftMin, - max: panel.grid.leftMax, - logBase: panel.grid.leftLogBase, - format: panel.y_formats[0], - label: panel.leftYAxisLabel, - }, - { - show: panel['y-axis'], - min: panel.grid.rightMin, - max: panel.grid.rightMax, - logBase: panel.grid.rightLogBase, - format: panel.y_formats[1], - label: panel.rightYAxisLabel, - }, - ]; - - panel.xaxis = { - show: panel['x-axis'], - }; - - delete panel.grid.leftMin; - delete panel.grid.leftMax; - delete panel.grid.leftLogBase; - delete panel.grid.rightMin; - delete panel.grid.rightMax; - delete panel.grid.rightLogBase; - delete panel.y_formats; - delete panel.leftYAxisLabel; - delete panel.rightYAxisLabel; - delete panel['y-axis']; - delete panel['x-axis']; - } - - return panel; - }); - } - if (oldVersion < 13 && finalTargetVersion >= 13) { // graph panel auto migrates to either barchart, bargauge, histogram or timeseries (all standard Grafana plugins) // see public/app/features/dashboard/state/getPanelPluginToMigrateTo.ts From 5cfb641ddf4351f95c62f872a77edabc565ce028 Mon Sep 17 00:00:00 2001 From: "alerting-team[bot]" <158350966+alerting-team[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 16:49:46 -0400 Subject: [PATCH 109/578] Alerting: Update alerting module to 9427c24835ae102fc54f2c07813a055d5f87156b (#112243) [create-pull-request] automated change Co-authored-by: yuri-tceretian <25988953+yuri-tceretian@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bd2255d8002..593f25c5698 100644 --- a/go.mod +++ b/go.mod @@ -86,7 +86,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.2 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20251007160934-e642236ea9eb // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics diff --git a/go.sum b/go.sum index 97fc36d91e2..ba9545692d5 100644 --- a/go.sum +++ b/go.sum @@ -1585,8 +1585,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/alerting v0.0.0-20251007160934-e642236ea9eb h1:ejpL3pI9t1ebEFtxaP7TMDKZagCu1nSq1O8op+sO4DY= -github.com/grafana/alerting v0.0.0-20251007160934-e642236ea9eb/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM= +github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae h1:NLPwY3tIP0lg0g9wTRiMcypm6VRXW6W+MOLBsq8JSVA= +github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= From bba720557c2d3541842d019f7f67405eee089e14 Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:58:47 -0600 Subject: [PATCH 110/578] Dashboard Migrations: v11 - no-op (#111273) * migrate to v19 * migrate to v18 * Migration to be verified: v17 Convert minSpan to maxPerRow in panels * Migration to be verified: 16 Grid layout migration * Refactor v17 and v19 migrations to use shared helper functions * Migration to be verified: 15 No-op migration for schema consistency * Migration to be verified: 14 Shared crosshair to graph tooltip migration * cleanup * wip * complete migration * fix lint issues * refactor and test with minimal graph config * update tests * migrate to v12 * extract defaults outside the func * lint * lint * add missing showValues prop * migrate to v11 * add test files * update * add context and fix latest version * add context * add context * generate snapshots * v13 should be no-op * clean up * fix tests * snapshots * fix test * remove v28 * remove singlestat migraiton from frontend migrator because this is an automigration * remove unused function * Remove v24 table plugin logic * cleanup * remove plugin version for automigrate as it was used only in v24 and v28 that have been removed * cleanup * update snapshot * es lint --------- Co-authored-by: Dominik Prokop --- .../pkg/migration/schemaversion/migrations.go | 3 +- .../pkg/migration/schemaversion/v11.go | 28 ++++ .../pkg/migration/schemaversion/v11_test.go | 38 ++++++ .../testdata/input/v11.no-op-migration.json | 58 +++++++++ .../v11.no-op-migration.v42.json | 121 ++++++++++++++++++ .../v11.no-op-migration.v11.json | 88 +++++++++++++ 6 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 apps/dashboard/pkg/migration/schemaversion/v11.go create mode 100644 apps/dashboard/pkg/migration/schemaversion/v11_test.go create mode 100644 apps/dashboard/pkg/migration/testdata/input/v11.no-op-migration.json create mode 100644 apps/dashboard/pkg/migration/testdata/output/latest_version/v11.no-op-migration.v42.json create mode 100644 apps/dashboard/pkg/migration/testdata/output/single_version/v11.no-op-migration.v11.json diff --git a/apps/dashboard/pkg/migration/schemaversion/migrations.go b/apps/dashboard/pkg/migration/schemaversion/migrations.go index 86ad0f8a631..360dcb02403 100644 --- a/apps/dashboard/pkg/migration/schemaversion/migrations.go +++ b/apps/dashboard/pkg/migration/schemaversion/migrations.go @@ -7,7 +7,7 @@ import ( ) const ( - MIN_VERSION = 11 + MIN_VERSION = 10 LATEST_VERSION = 42 ) @@ -35,6 +35,7 @@ type PanelPluginInfo struct { func GetMigrations(dsInfoProvider DataSourceInfoProvider) map[int]SchemaVersionMigrationFunc { return map[int]SchemaVersionMigrationFunc{ + 11: V11, 12: V12, 13: V13, 14: V14, diff --git a/apps/dashboard/pkg/migration/schemaversion/v11.go b/apps/dashboard/pkg/migration/schemaversion/v11.go new file mode 100644 index 00000000000..a31e13315a0 --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v11.go @@ -0,0 +1,28 @@ +package schemaversion + +import "context" + +// V11 migration is a no-op migration +// It only updates the schema version to 11 +// It's created to keep the migration history consistent +// Frontend migrator doesn't have a migration for schema version 11 +// No specific migration logic is needed between schema version 10 and 11 + +// Example before migration: +// { +// "schemaVersion": 10, +// "title": "My Dashboard", +// "panels": [...] +// } + +// Example after migration: +// { +// "schemaVersion": 11, +// "title": "My Dashboard", +// "panels": [...] +// } + +func V11(_ context.Context, dashboard map[string]interface{}) error { + dashboard["schemaVersion"] = 11 + return nil +} diff --git a/apps/dashboard/pkg/migration/schemaversion/v11_test.go b/apps/dashboard/pkg/migration/schemaversion/v11_test.go new file mode 100644 index 00000000000..d357b141639 --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v11_test.go @@ -0,0 +1,38 @@ +package schemaversion_test + +import ( + "testing" + + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" +) + +func TestV11(t *testing.T) { + tests := []migrationTestCase{ + { + name: "v11 no-op migration, updates schema version only", + input: map[string]interface{}{ + "title": "V11 No-Op Migration Test Dashboard", + "schemaVersion": 10, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "title": "Panel remains unchanged", + "id": 1, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V11 No-Op Migration Test Dashboard", + "schemaVersion": 11, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "title": "Panel remains unchanged", + "id": 1, + }, + }, + }, + }, + } + runMigrationTests(t, tests, schemaversion.V11) +} diff --git a/apps/dashboard/pkg/migration/testdata/input/v11.no-op-migration.json b/apps/dashboard/pkg/migration/testdata/input/v11.no-op-migration.json new file mode 100644 index 00000000000..82b054d9d11 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/input/v11.no-op-migration.json @@ -0,0 +1,58 @@ +{ + "title": "V11 No-Op Migration Test Dashboard", + "schemaVersion": 10, + "panels": [ + { + "type": "graph", + "title": "CPU Usage", + "id": 1, + "yAxes": [ + { + "show": true, + "min": null, + "max": null + } + ] + }, + { + "type": "singlestat", + "title": "Memory Usage", + "id": 2, + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ] + } + ], + "templating": { + "list": [ + { + "name": "server", + "type": "query", + "datasource": "prometheus", + "query": "label_values(server)", + "refresh": 1, + "options": [] + } + ] + }, + "annotations": { + "list": [ + { + "name": "Annotations & Alerts", + "datasource": "grafana", + "enable": true + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] + } + } diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v11.no-op-migration.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v11.no-op-migration.v42.json new file mode 100644 index 00000000000..cde0f37b8a3 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v11.no-op-migration.v42.json @@ -0,0 +1,121 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + }, + { + "datasource": { + "uid": "grafana" + }, + "enable": true, + "name": "Annotations \u0026 Alerts" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "CPU Usage", + "type": "timeseries", + "yAxes": [ + { + "show": true + } + ] + }, + { + "autoMigrateFrom": "singlestat", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Memory Usage", + "type": "stat", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ] + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "datasource": "prometheus", + "name": "server", + "options": [], + "query": "label_values(server)", + "refresh": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "V11 No-Op Migration Test Dashboard", + "weekStart": "" +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v11.no-op-migration.v11.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v11.no-op-migration.v11.json new file mode 100644 index 00000000000..7e6680140e0 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v11.no-op-migration.v11.json @@ -0,0 +1,88 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + }, + { + "datasource": "grafana", + "enable": true, + "name": "Annotations \u0026 Alerts" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "graph", + "id": 1, + "title": "CPU Usage", + "type": "timeseries", + "yAxes": [ + { + "show": true + } + ] + }, + { + "autoMigrateFrom": "singlestat", + "id": 2, + "title": "Memory Usage", + "type": "stat", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ] + } + ], + "schemaVersion": 11, + "tags": [], + "templating": { + "list": [ + { + "datasource": "prometheus", + "name": "server", + "options": [], + "query": "label_values(server)", + "refresh": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "V11 No-Op Migration Test Dashboard", + "weekStart": "" +} \ No newline at end of file From 01ec8e3a4aa74ae730281876031a2a97f85bf2cb Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Thu, 9 Oct 2025 16:36:24 -0600 Subject: [PATCH 111/578] Dashboard Migrations: V10 - table panel styles.thresholds (#111420) * migrate to v19 * migrate to v18 * Migration to be verified: v17 Convert minSpan to maxPerRow in panels * Migration to be verified: 16 Grid layout migration * Refactor v17 and v19 migrations to use shared helper functions * Migration to be verified: 15 No-op migration for schema consistency * Migration to be verified: 14 Shared crosshair to graph tooltip migration * cleanup * wip * complete migration * fix lint issues * refactor and test with minimal graph config * update tests * migrate to v12 * extract defaults outside the func * lint * lint * add missing showValues prop * migrate to v11 * migrate to v10 * add test files * update * add context and fix latest version * add context * add context * generate snapshots * v13 should be no-op * clean up * fix tests * add context * snapshots * generate snapshots * fix test * remove v28 * remove singlestat migraiton from frontend migrator because this is an automigration * remove unused function * Remove v24 table plugin logic * cleanup * remove plugin version for automigrate as it was used only in v24 and v28 that have been removed * cleanup * update snapshot * update snapshot --------- Co-authored-by: Dominik Prokop --- .../pkg/migration/schemaversion/migrations.go | 3 +- .../pkg/migration/schemaversion/v10.go | 97 ++++++++ .../pkg/migration/schemaversion/v10_test.go | 209 ++++++++++++++++++ .../testdata/input/v10.table_thresholds.json | 36 +++ .../v10.table_thresholds.v42.json | 132 +++++++++++ .../v10.table_thresholds.v10.json | 81 +++++++ 6 files changed, 557 insertions(+), 1 deletion(-) create mode 100644 apps/dashboard/pkg/migration/schemaversion/v10.go create mode 100644 apps/dashboard/pkg/migration/schemaversion/v10_test.go create mode 100644 apps/dashboard/pkg/migration/testdata/input/v10.table_thresholds.json create mode 100644 apps/dashboard/pkg/migration/testdata/output/latest_version/v10.table_thresholds.v42.json create mode 100644 apps/dashboard/pkg/migration/testdata/output/single_version/v10.table_thresholds.v10.json diff --git a/apps/dashboard/pkg/migration/schemaversion/migrations.go b/apps/dashboard/pkg/migration/schemaversion/migrations.go index 360dcb02403..bc2ddfdd130 100644 --- a/apps/dashboard/pkg/migration/schemaversion/migrations.go +++ b/apps/dashboard/pkg/migration/schemaversion/migrations.go @@ -7,7 +7,7 @@ import ( ) const ( - MIN_VERSION = 10 + MIN_VERSION = 9 LATEST_VERSION = 42 ) @@ -35,6 +35,7 @@ type PanelPluginInfo struct { func GetMigrations(dsInfoProvider DataSourceInfoProvider) map[int]SchemaVersionMigrationFunc { return map[int]SchemaVersionMigrationFunc{ + 10: V10, 11: V11, 12: V12, 13: V13, diff --git a/apps/dashboard/pkg/migration/schemaversion/v10.go b/apps/dashboard/pkg/migration/schemaversion/v10.go new file mode 100644 index 00000000000..6b218460aac --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v10.go @@ -0,0 +1,97 @@ +package schemaversion + +import "context" + +// V10 migration removes the first threshold value from table panel styles when they have 3 or more thresholds. +// This migration aligns with the frontend schema version 10 changes that addressed aliasYAxis changes +// specifically for table panels with threshold configurations. +// +// Background: +// In earlier versions, table panels stored threshold values as arrays with the first element representing +// a baseline value that was not actually used in threshold calculations. This migration removes that +// unused first element to clean up the data structure. +// +// Example before migration: +// { +// "schemaVersion": 9, +// "panels": [ +// { +// "type": "table", +// "styles": [ +// { +// "thresholds": ["10", "20", "30"] +// }, +// { +// "thresholds": ["100", "200", "300"] +// } +// ] +// } +// ] +// } +// +// Example after migration: +// { +// "schemaVersion": 10, +// "panels": [ +// { +// "type": "table", +// "styles": [ +// { +// "thresholds": ["20", "30"] +// }, +// { +// "thresholds": ["200", "300"] +// } +// ] +// } +// ] +// } + +func V10(_ context.Context, dashboard map[string]interface{}) error { + dashboard["schemaVersion"] = 10 + + panels, ok := dashboard["panels"].([]interface{}) + if !ok { + return nil + } + + for _, p := range panels { + panel, ok := p.(map[string]interface{}) + if !ok { + continue + } + + // Only process table panels + panelType := GetStringValue(panel, "type") + if panelType != "table" { + continue + } + + styles, ok := panel["styles"].([]interface{}) + if !ok { + continue + } + + // Process each style in the table panel + for _, s := range styles { + style, ok := s.(map[string]interface{}) + if !ok { + continue + } + + thresholds, ok := style["thresholds"].([]interface{}) + if !ok { + continue + } + + // Only modify thresholds if they have 3 or more values + if len(thresholds) >= 3 { + // Remove the first threshold value + newThresholds := thresholds[1:] + style["thresholds"] = newThresholds + } + } + } + + return nil +} diff --git a/apps/dashboard/pkg/migration/schemaversion/v10_test.go b/apps/dashboard/pkg/migration/schemaversion/v10_test.go new file mode 100644 index 00000000000..a1bb57cf94f --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v10_test.go @@ -0,0 +1,209 @@ +package schemaversion_test + +import ( + "testing" + + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" +) + +func TestV10(t *testing.T) { + tests := []migrationTestCase{ + { + name: "table panel with thresholds having 3 or more values should have first threshold removed", + input: map[string]interface{}{ + "title": "V10 Table Thresholds Migration Test Dashboard", + "schemaVersion": 9, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "id": 1, + "styles": []interface{}{ + map[string]interface{}{ + "thresholds": []interface{}{"10", "20", "30"}, + }, + map[string]interface{}{ + "thresholds": []interface{}{"100", "200", "300"}, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V10 Table Thresholds Migration Test Dashboard", + "schemaVersion": 10, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "id": 1, + "styles": []interface{}{ + map[string]interface{}{ + "thresholds": []interface{}{"20", "30"}, + }, + map[string]interface{}{ + "thresholds": []interface{}{"200", "300"}, + }, + }, + }, + }, + }, + }, + { + name: "table panel with thresholds having less than 3 values should remain unchanged", + input: map[string]interface{}{ + "title": "V10 Table Thresholds No Change Test Dashboard", + "schemaVersion": 9, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "id": 1, + "styles": []interface{}{ + map[string]interface{}{ + "thresholds": []interface{}{"10", "20"}, + }, + map[string]interface{}{ + "thresholds": []interface{}{"100"}, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V10 Table Thresholds No Change Test Dashboard", + "schemaVersion": 10, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "id": 1, + "styles": []interface{}{ + map[string]interface{}{ + "thresholds": []interface{}{"10", "20"}, + }, + map[string]interface{}{ + "thresholds": []interface{}{"100"}, + }, + }, + }, + }, + }, + }, + { + name: "non-table panels should remain unchanged", + input: map[string]interface{}{ + "title": "V10 Non-Table Panel Test Dashboard", + "schemaVersion": 9, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "id": 1, + "styles": []interface{}{ + map[string]interface{}{ + "thresholds": []interface{}{"10", "20", "30"}, + }, + }, + }, + map[string]interface{}{ + "type": "singlestat", + "id": 2, + "styles": []interface{}{ + map[string]interface{}{ + "thresholds": []interface{}{"100", "200", "300"}, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V10 Non-Table Panel Test Dashboard", + "schemaVersion": 10, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "id": 1, + "styles": []interface{}{ + map[string]interface{}{ + "thresholds": []interface{}{"10", "20", "30"}, + }, + }, + }, + map[string]interface{}{ + "type": "singlestat", + "id": 2, + "styles": []interface{}{ + map[string]interface{}{ + "thresholds": []interface{}{"100", "200", "300"}, + }, + }, + }, + }, + }, + }, + { + name: "table panel without styles should remain unchanged", + input: map[string]interface{}{ + "title": "V10 Table No Styles Test Dashboard", + "schemaVersion": 9, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "id": 1, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V10 Table No Styles Test Dashboard", + "schemaVersion": 10, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "id": 1, + }, + }, + }, + }, + { + name: "table panel with styles but no thresholds should remain unchanged", + input: map[string]interface{}{ + "title": "V10 Table No Thresholds Test Dashboard", + "schemaVersion": 9, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "id": 1, + "styles": []interface{}{ + map[string]interface{}{ + "colorMode": "cell", + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V10 Table No Thresholds Test Dashboard", + "schemaVersion": 10, + "panels": []interface{}{ + map[string]interface{}{ + "type": "table", + "id": 1, + "styles": []interface{}{ + map[string]interface{}{ + "colorMode": "cell", + }, + }, + }, + }, + }, + }, + { + name: "dashboard without panels should only update schema version", + input: map[string]interface{}{ + "title": "V10 No Panels Test Dashboard", + "schemaVersion": 9, + }, + expected: map[string]interface{}{ + "title": "V10 No Panels Test Dashboard", + "schemaVersion": 10, + }, + }, + } + runMigrationTests(t, tests, schemaversion.V10) +} diff --git a/apps/dashboard/pkg/migration/testdata/input/v10.table_thresholds.json b/apps/dashboard/pkg/migration/testdata/input/v10.table_thresholds.json new file mode 100644 index 00000000000..065423b9fea --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/input/v10.table_thresholds.json @@ -0,0 +1,36 @@ +{ + "title": "V10 Table Thresholds Test", + "schemaVersion": 9, + "panels": [ + { + "id": 1, + "type": "table", + "styles": [ + { + "thresholds": ["10", "20", "30"] + }, + { + "thresholds": ["100", "200", "300"] + } + ] + }, + { + "id": 2, + "type": "table", + "styles": [ + { + "thresholds": ["50", "75"] + } + ] + }, + { + "id": 3, + "type": "graph", + "styles": [ + { + "thresholds": ["5", "10", "15"] + } + ] + } + ] +} diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v10.table_thresholds.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v10.table_thresholds.v42.json new file mode 100644 index 00000000000..3f17bcd374c --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v10.table_thresholds.v42.json @@ -0,0 +1,132 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "table-old", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "styles": [ + { + "align": "auto", + "thresholds": [ + "20", + "30" + ] + }, + { + "align": "auto", + "thresholds": [ + "200", + "300" + ] + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "table" + }, + { + "autoMigrateFrom": "table-old", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "styles": [ + { + "align": "auto", + "thresholds": [ + "50", + "75" + ] + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "table" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "styles": [ + { + "thresholds": [ + "5", + "10", + "15" + ] + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V10 Table Thresholds Test", + "weekStart": "" +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v10.table_thresholds.v10.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v10.table_thresholds.v10.json new file mode 100644 index 00000000000..2e178e2e129 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v10.table_thresholds.v10.json @@ -0,0 +1,81 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "id": 1, + "styles": [ + { + "thresholds": [ + "20", + "30" + ] + }, + { + "thresholds": [ + "200", + "300" + ] + } + ], + "type": "table" + }, + { + "id": 2, + "styles": [ + { + "thresholds": [ + "50", + "75" + ] + } + ], + "type": "table" + }, + { + "autoMigrateFrom": "graph", + "id": 3, + "styles": [ + { + "thresholds": [ + "5", + "10", + "15" + ] + } + ], + "type": "timeseries" + } + ], + "schemaVersion": 10, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V10 Table Thresholds Test", + "weekStart": "" +} \ No newline at end of file From cfaeec4854cd9276323dcf62ba5005d8678baa3d Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Fri, 10 Oct 2025 10:30:33 +0200 Subject: [PATCH 112/578] Dynamic dashboards: Ungroup all rows in a row layout to increase discoverability of ungrouping rows. (#110109) * spike * Improvements * Let users choose what grid to convert to * fix lint * make sure we don't get multiple undo entries when ungrouping. Also move cancel button * updates from review * Clear parent when merging default grid --- .../dashboard-scene/scene/DashboardScene.tsx | 21 ++- .../AutoGridLayoutManager.tsx | 16 ++ .../DefaultGridLayoutManager.tsx | 29 +++ .../layout-rows/ConvertMixedGridsModal.tsx | 48 +++++ .../scene/layout-rows/RowsLayoutManager.tsx | 178 ++++++++++++++++-- .../layout-rows/RowsLayoutManagerRenderer.tsx | 3 + .../scene/layouts-shared/findAllGridTypes.ts | 17 ++ .../scene/layouts-shared/utils.ts | 4 +- .../scene/types/DashboardLayoutManager.ts | 5 + .../scene/types/LayoutParent.ts | 5 +- public/locales/en-US/grafana.json | 15 +- 11 files changed, 314 insertions(+), 27 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/layout-rows/ConvertMixedGridsModal.tsx create mode 100644 public/app/features/dashboard-scene/scene/layouts-shared/findAllGridTypes.ts diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index f918af82afa..61b5e7aa948 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -629,15 +629,20 @@ export class DashboardScene extends SceneObjectBase impleme return vizPanel; } - public switchLayout(layout: DashboardLayoutManager) { + public switchLayout(layout: DashboardLayoutManager, skipUndo?: boolean) { const currentLayout = this.state.body; - - dashboardEditActions.edit({ - description: t('dashboard.edit-actions.switch-layout', 'Switch layout'), - source: this, - perform: () => this.setState({ body: layout }), - undo: () => this.setState({ body: currentLayout }), - }); + const perform = () => this.setState({ body: layout }); + const undo = () => this.setState({ body: currentLayout }); + if (skipUndo) { + perform(); + } else { + dashboardEditActions.edit({ + description: t('dashboard.edit-actions.switch-layout', 'Switch layout'), + source: this, + perform, + undo, + }); + } } public getLayout(): DashboardLayoutManager { diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx index 397c0ce6bb8..a83da646ca8 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx @@ -200,6 +200,22 @@ export class AutoGridLayoutManager }); } + public merge(other: DashboardLayoutManager) { + if (!(other instanceof AutoGridLayoutManager)) { + throw new Error('Cannot merge non-auto grid layout'); + } + + const sourceLayout = other.state.layout; + const movedChildren = [...sourceLayout.state.children]; + + // Remove from source and append to destination + sourceLayout.setState({ children: [] }); + movedChildren.forEach((child) => { + child.clearParent(); + }); + this.state.layout.setState({ children: [...this.state.layout.state.children, ...movedChildren] }); + } + public duplicatePanel(panel: VizPanel) { const gridItem = panel.parent; if (!(gridItem instanceof AutoGridItem)) { diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index ad3930e1aab..886dd0a9391 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -93,6 +93,35 @@ export class DefaultGridLayoutManager this.addActivationHandler(() => this._activationHandler()); } + public merge(other: DashboardLayoutManager) { + if (!(other instanceof DefaultGridLayoutManager)) { + throw new Error('Cannot merge non-default grid layout'); + } + + let offset = 0; + for (const child of this.state.grid.state.children) { + const newOffset = (child.state.y ?? 0) + (child.state.height ?? 0); + if (newOffset > offset) { + offset = newOffset; + } + } + + const sourceGrid = other.state.grid; + const movedChildren = [...sourceGrid.state.children]; + + for (const child of movedChildren) { + const currentY = child.state.y ?? 0; + child.setState({ y: currentY + offset }); + } + + // Remove from source and append to destination + sourceGrid.setState({ children: [] }); + for (const child of movedChildren) { + child.clearParent(); + } + this.state.grid.setState({ children: [...this.state.grid.state.children, ...movedChildren] }); + } + private _activationHandler() { if (config.featureToggles.dashboardNewLayouts) { this._subs.add( diff --git a/public/app/features/dashboard-scene/scene/layout-rows/ConvertMixedGridsModal.tsx b/public/app/features/dashboard-scene/scene/layout-rows/ConvertMixedGridsModal.tsx new file mode 100644 index 00000000000..d13eb273522 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-rows/ConvertMixedGridsModal.tsx @@ -0,0 +1,48 @@ +import { t, Trans } from '@grafana/i18n'; +import { Modal, Button } from '@grafana/ui'; + +import { layoutRegistry } from '../layouts-shared/layoutRegistry'; + +interface ConvertMixedGridsModalProps { + availableIds: Set; + onSelect: (id: string) => void; + onDismiss: () => void; +} + +export function ConvertMixedGridsModal({ availableIds, onSelect, onDismiss }: ConvertMixedGridsModalProps) { + const options = layoutRegistry.list(Array.from(availableIds)); + + return ( + +

    + + All grids must be converted to the same type and positions will be lost. + +

    + + + {options.map((opt) => ( + + ))} + +
    + ); +} diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx index d021eac7889..fbefcfbbc64 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -9,19 +9,25 @@ import { VizPanel, } from '@grafana/scenes'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; +import appEvents from 'app/core/app_events'; +import { ShowConfirmModalEvent, ShowModalReactEvent } from 'app/types/events'; import { dashboardEditActions, ObjectsReorderedOnCanvasEvent } from '../../edit-pane/shared'; import { serializeRowsLayout } from '../../serialization/layoutSerializers/RowsLayoutSerializer'; import { getDashboardSceneFor } from '../../utils/utils'; +import { AutoGridLayoutManager } from '../layout-auto-grid/AutoGridLayoutManager'; import { DashboardGridItem } from '../layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; import { RowRepeaterBehavior } from '../layout-default/RowRepeaterBehavior'; import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; +import { findAllGridTypes } from '../layouts-shared/findAllGridTypes'; import { getRowFromClipboard } from '../layouts-shared/paste'; import { generateUniqueTitle, ungroupLayout } from '../layouts-shared/utils'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; +import { isLayoutParent } from '../types/LayoutParent'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; +import { ConvertMixedGridsModal } from './ConvertMixedGridsModal'; import { RowItem } from './RowItem'; import { RowLayoutManagerRenderer } from './RowsLayoutManagerRenderer'; @@ -29,6 +35,22 @@ interface RowsLayoutManagerState extends SceneObjectState { rows: RowItem[]; } +enum GridLayoutType { + AutoGridLayout = 'AutoGridLayout', + GridLayout = 'GridLayout', +} + +function mapIdToGridLayoutType(id?: string): GridLayoutType | undefined { + switch (id) { + case GridLayoutType.AutoGridLayout: + return GridLayoutType.AutoGridLayout; + case GridLayoutType.GridLayout: + return GridLayoutType.GridLayout; + default: + return undefined; + } +} + export class RowsLayoutManager extends SceneObjectBase implements DashboardLayoutManager { public static Component = RowLayoutManagerRenderer; public readonly isDashboardLayoutManager = true; @@ -128,25 +150,155 @@ export class RowsLayoutManager extends SceneObjectBase i return outlineChildren; } - public removeRow(row: RowItem) { + public convertAllRowsLayouts(gridLayoutType: GridLayoutType) { + for (const row of this.state.rows) { + switch (gridLayoutType) { + case GridLayoutType.AutoGridLayout: + if (!(row.getLayout() instanceof AutoGridLayoutManager)) { + row.switchLayout(AutoGridLayoutManager.createFromLayout(row.getLayout())); + } + break; + case GridLayoutType.GridLayout: + if (!(row.getLayout() instanceof DefaultGridLayoutManager)) { + row.switchLayout(DefaultGridLayoutManager.createFromLayout(row.getLayout())); + } + break; + } + } + } + + public ungroupRows() { + const hasNonGridLayout = this.state.rows.some((row) => !row.getLayout().descriptor.isGridLayout); + const gridTypes = new Set(findAllGridTypes(this)); + + if (hasNonGridLayout) { + appEvents.publish( + new ShowConfirmModalEvent({ + title: t('dashboard.rows-layout.ungroup-nested-title', 'Ungroup nested groups?'), + text: t('dashboard.rows-layout.ungroup-nested-text', 'This will ungroup all nested groups.'), + yesText: t('dashboard.rows-layout.continue', 'Continue'), + noText: t('dashboard.rows-layout.cancel', 'Cancel'), + onConfirm: () => { + if (gridTypes.size > 1) { + requestAnimationFrame(() => { + this._confirmConvertMixedGrids(gridTypes); + }); + } else { + this.wrapUngroupRowsInEdit(mapIdToGridLayoutType(gridTypes.values().next().value)!); + } + }, + }) + ); + return; + } + + if (gridTypes.size > 1) { + this._confirmConvertMixedGrids(gridTypes); + return; + } else { + this.wrapUngroupRowsInEdit(mapIdToGridLayoutType(gridTypes.values().next().value)!); + } + } + + private _confirmConvertMixedGrids(availableIds: Set) { + appEvents.publish( + new ShowModalReactEvent({ + component: ConvertMixedGridsModal, + props: { + availableIds, + onSelect: (id: string) => { + const selected = mapIdToGridLayoutType(id); + if (selected) { + this.wrapUngroupRowsInEdit(selected); + } + }, + }, + }) + ); + } + + private wrapUngroupRowsInEdit(gridLayoutType: GridLayoutType) { + const parent = this.parent; + if (!parent || !isLayoutParent(parent)) { + throw new Error('Ungroup rows failed: parent is not a layout container'); + } + + const previousLayout = this.clone({}); + const scene = getDashboardSceneFor(this); + + dashboardEditActions.edit({ + description: t('dashboard.rows-layout.edit.ungroup-rows', 'Ungroup rows'), + source: scene, + perform: () => { + this._ungroupRows(gridLayoutType); + }, + undo: () => { + parent.switchLayout(previousLayout); + }, + }); + } + + private _ungroupRows(gridLayoutType: GridLayoutType) { + const hasNonGridLayout = this.state.rows.some((row) => !row.getLayout().descriptor.isGridLayout); + + if (hasNonGridLayout) { + for (const row of this.state.rows) { + const layout = row.getLayout(); + if (!layout.descriptor.isGridLayout) { + if (layout instanceof RowsLayoutManager) { + layout._ungroupRows(gridLayoutType); + } else { + throw new Error(`Ungrouping not supported for layout type: ${layout.descriptor.name}`); + } + } + } + } + + this.convertAllRowsLayouts(gridLayoutType); + + const firstRow = this.state.rows[0]; + const firstRowLayout = firstRow.getLayout(); + const otherRows = this.state.rows.slice(1); + + for (const row of otherRows) { + const layout = row.getLayout(); + if (firstRowLayout.merge) { + firstRowLayout.merge(layout); + } else { + throw new Error(`Layout type ${firstRowLayout.descriptor.name} does not support merging`); + } + } + + this.setState({ rows: [firstRow] }); + this.removeRow(firstRow, true); + } + + public removeRow(row: RowItem, skipUndo?: boolean) { // When removing last row replace ourselves with the inner row layout if (this.shouldUngroup()) { - ungroupLayout(this, row.state.layout); + ungroupLayout(this, row.state.layout, skipUndo ?? false); return; } const indexOfRowToRemove = this.state.rows.findIndex((r) => r === row); - dashboardEditActions.removeElement({ - removedObject: row, - source: this, - perform: () => this.setState({ rows: this.state.rows.filter((r) => r !== row) }), - undo: () => { - const rows = [...this.state.rows]; - rows.splice(indexOfRowToRemove, 0, row); - this.setState({ rows }); - }, - }); + const perform = () => this.setState({ rows: this.state.rows.filter((r) => r !== row) }); + const undo = () => { + const rows = [...this.state.rows]; + rows.splice(indexOfRowToRemove, 0, row); + this.setState({ rows }); + }; + + if (skipUndo) { + perform(); + } else { + dashboardEditActions.removeElement({ + removedObject: row, + source: this, + perform, + undo, + }); + } } public moveRow(_rowKey: string, fromIndex: number, toIndex: number) { @@ -212,7 +364,7 @@ export class RowsLayoutManager extends SceneObjectBase i if (child instanceof SceneGridRow) { // Skip repeated row clones - if (child.state.repeatSourceKey) { + if ('repeatSourceKey' in child.state && child.state.repeatSourceKey) { return; } diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx index b18634bdd00..d7de6c42413 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx @@ -53,6 +53,9 @@ export function RowLayoutManagerRenderer({ model }: SceneComponentProps + diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx index 125bd380256..f4714e7d420 100644 --- a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx +++ b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx @@ -1,10 +1,12 @@ import { useCallback, useMemo } from 'react'; import { t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; import { RadioButtonGroup, Box } from '@grafana/ui'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; +import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { isLayoutParent } from '../types/LayoutParent'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; @@ -19,6 +21,21 @@ export function DashboardLayoutSelector({ layoutManager }: Props) { const isGridLayout = layoutManager.descriptor.isGridLayout; const options = layoutRegistry.list().filter((layout) => layout.isGridLayout === isGridLayout); + const disableTabs = useMemo(() => { + if (config.featureToggles.unlimitedLayoutsNesting) { + return false; + } + let parent = layoutManager.parent; + while (parent) { + if (parent instanceof TabsLayoutManager) { + return true; + } + parent = parent.parent; + } + + return false; + }, [layoutManager]); + const onChangeLayout = useCallback( (newLayout: LayoutRegistryItem) => { const layoutParent = layoutManager.parent; @@ -30,17 +47,33 @@ export function DashboardLayoutSelector({ layoutManager }: Props) { [layoutManager] ); - const radioOptions = options.map((opt) => ({ - value: opt, - label: opt.name, - icon: opt.icon, - description: opt.description, - ariaLabel: `layout-selection-option-${opt.name}`, - })); + const disabledOptions: LayoutRegistryItem[] = []; + + const radioOptions = options.map((opt) => { + let description = opt.description; + if (disableTabs && opt.id === TabsLayoutManager.descriptor.id) { + description = t('dashboard.canvas-actions.disabled-nested-tabs', 'Tabs cannot be nested inside other tabs'); + disabledOptions.push(opt); + } + + return { + value: opt, + label: opt.name, + icon: opt.icon, + description, + ariaLabel: `layout-selection-option-${opt.name}`, + }; + }); return ( - + ); } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 865e80a7934..189ec56399c 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -4496,6 +4496,8 @@ }, "canvas-actions": { "add-panel": "Add panel", + "disabled-nested-grouping": "Grouping is limited to 2 levels", + "disabled-nested-tabs": "Tabs cannot be nested inside other tabs", "group-into-row": "Group into row", "group-into-tab": "Group into tab", "group-panels": "Group panels", From e05cfb676af09414696eceebe95a8d61da08ccc2 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 12:22:42 +0000 Subject: [PATCH 123/578] I18n: Download translations from Crowdin (#112251) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 38 +++++++++++++++++------------ public/locales/de-DE/grafana.json | 38 +++++++++++++++++------------ public/locales/es-ES/grafana.json | 38 +++++++++++++++++------------ public/locales/fr-FR/grafana.json | 38 +++++++++++++++++------------ public/locales/hu-HU/grafana.json | 38 +++++++++++++++++------------ public/locales/id-ID/grafana.json | 38 +++++++++++++++++------------ public/locales/it-IT/grafana.json | 38 +++++++++++++++++------------ public/locales/ja-JP/grafana.json | 38 +++++++++++++++++------------ public/locales/ko-KR/grafana.json | 38 +++++++++++++++++------------ public/locales/nl-NL/grafana.json | 38 +++++++++++++++++------------ public/locales/pl-PL/grafana.json | 38 +++++++++++++++++------------ public/locales/pt-BR/grafana.json | 38 +++++++++++++++++------------ public/locales/pt-PT/grafana.json | 38 +++++++++++++++++------------ public/locales/ru-RU/grafana.json | 38 +++++++++++++++++------------ public/locales/sv-SE/grafana.json | 38 +++++++++++++++++------------ public/locales/tr-TR/grafana.json | 38 +++++++++++++++++------------ public/locales/zh-Hans/grafana.json | 38 +++++++++++++++++------------ public/locales/zh-Hant/grafana.json | 38 +++++++++++++++++------------ 18 files changed, 414 insertions(+), 270 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 816818e14df..bde778d53e0 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "Přidat oprávnění", + "loading": "", "no-permissions": "Nejsou k dispozici žádná oprávnění", "permissions-change-warning": "Tímto změníte oprávnění pro tuto složku a všechny následné složky. Celkově to ovlivní:", "role": "Role", "serviceaccount": "Účet služby", "team": "Tým", - "title": "Oprávnění", "user": "Uživatel" } }, @@ -3528,7 +3528,7 @@ "delete-button": "Odstranit", "delete-modal-invalid-text": "Jedna nebo více složek obsahuje panely knihovny nebo pravidla výstrah. Chcete-li pokračovat, nejprve je odstraňte.", "delete-modal-invalid-title": "Složku nelze odstranit", - "delete-modal-restore-dashboards-text": "Tato akce okamžitě odstraní vybrané složky, ale vybrané nástěnky budou označeny k odstranění za 30 dnů. Správce organizace může obnovit nástěnky kdykoli před uplynutím 30 dnů. Složky nelze obnovit.", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "Tato akce odstraní následující obsah:", "delete-modal-title": "Odstranit", "delete-provisioned-folder": "Odstranit poskytnutou složku", @@ -4612,7 +4612,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "Prvek je skrytý kvůli podmíněnému vykreslení." + "tooltip": "" }, "root": { "title": "Zobrazit/skrýt pravidla" @@ -5861,6 +5861,7 @@ "dynamically-switch-source-multiple-panels": "Dynamicky přepínat zdroj dat pro více panelů", "group": "Přidat klíče do skupiny za chodu", "hidden-constant-variable": "Skrytá konstantní proměnná, užitečná pro metrické předpony na nástěnkách, které chcete sdílet", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "Uživatelé mohou do textového pole zadat libovolné řetězce znaků", "values-are-static-and-defined-manually": "Hodnoty jsou statické a definované ručně", "values-fetched-source-query": "Hodnoty jsou načteny z dotazu na zdroj dat", @@ -5874,6 +5875,7 @@ "group-by": "Seskupit podle", "interval": "Interval", "query": "Dotaz", + "switch": "", "textbox": "Textové pole" } }, @@ -6222,6 +6224,17 @@ "copy-to-clipboard-failed": "Kopírování do schránky se nezdařilo" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "Výchozí hodnota" }, @@ -10417,7 +10430,7 @@ "title": "Profily" }, "recently-deleted": { - "subtitle": "Všechny tady uvedené položky po dobu delší než 30 dnů budou automaticky odstraněny.", + "subtitle": "", "title": "Nedávno odstraněné" }, "recorded-queries": { @@ -11651,6 +11664,7 @@ "active-jobs": "aktivní práce", "column-action": "Akce", "column-duration": "Doba trvání", + "column-job-id": "", "column-message": "Zpráva", "column-started": "Zahájeno", "column-status": "Stav", @@ -11669,12 +11683,6 @@ "settings": "Nastavení", "view": "Zobrazit" }, - "repository-health": { - "details": "Podrobnosti:", - "no-errors-found": "Nebyly nalezeny žádné chyby", - "title-repository-is-healthy": "Úložiště je zdravé", - "title-repository-is-unhealthy": "Úložiště je nezdravé" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "Porovnat větev", @@ -11689,7 +11697,7 @@ }, "repository-overview": { "checked": "Zkontrolováno:", - "finished": "Dokončeno:", + "finished": "", "health": "Kondice", "healthy": "Zdravý", "job-id": "ID úkolu:", @@ -11698,7 +11706,6 @@ "not-available": "Nelze aplikovat", "pull-status": "Stav stažení", "resources": "Zdroje", - "started": "Zahájeno:", "status": "Stav:", "unhealthy": "Nezdravý", "view-folder": "Zobrazit složku", @@ -12364,9 +12371,6 @@ "tokens": "Tokeny", "tooltip-managed-service-account-cannot-modified": "Toto je spravovaný účet služby a nelze ho upravit" }, - "service-account-permissions": { - "title-permissions": "Oprávnění" - }, "service-account-profile": { "information": "Informace", "label-creation-date": "Datum vytvoření", @@ -13045,6 +13049,10 @@ "name-show-line-numbers": "Zobrazovat čísla řádek", "name-show-mini-map": "Zobrazit mini mapu" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "Nástěnky", diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index af72317801a..1cb6f9fc68f 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "Berechtigung hinzufügen", + "loading": "", "no-permissions": "Keine Berechtigungen vorhanden", "permissions-change-warning": "Dadurch werden die Berechtigungen für diesen Ordner und alle seine Unterordner geändert. Insgesamt betrifft dies Folgendes:", "role": "Rolle", "serviceaccount": "Service-Konto", "team": "Team", - "title": "Berechtigung", "user": "Nutzer" } }, @@ -3504,7 +3504,7 @@ "delete-button": "Löschen", "delete-modal-invalid-text": "Ein oder mehrere Ordner enthalten Bibliotheksfenster oder Alarmierungsregeln. Löschen Sie diese zuerst, um fortzufahren.", "delete-modal-invalid-title": "Ordner kann nicht gelöscht werden", - "delete-modal-restore-dashboards-text": "Durch diese Aktion werden die ausgewählten Ordner sofort gelöscht, aber die ausgewählten Dashboards werden innerhalb von 30 Tagen zur Löschung markiert. Der Administrator Ihrer Organisation kann die Dashboards jederzeit vor Ablauf der 30 Tage wiederherstellen. Ordner können nicht wiederhergestellt werden.", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "Diese Aktion wird folgenden Inhalt löschen:", "delete-modal-title": "Löschen", "delete-provisioned-folder": "Bereitgestellten Ordner löschen", @@ -4572,7 +4572,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "Das Element wird aufgrund von bedingtem Rendering ausgeblendet." + "tooltip": "" }, "root": { "title": "Regeln ein-/ausblenden" @@ -5819,6 +5819,7 @@ "dynamically-switch-source-multiple-panels": "Dynamisches Wechseln der Datenquelle für mehrere Panels", "group": "Fügen Sie spontan Schlüssel zum Gruppieren hinzu", "hidden-constant-variable": "Eine ausgeblendete konstante Variable, nützlich für metrische Präfixe in Dashboards, die Sie teilen möchten", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "Nutzer können beliebige Zeichenfolgen in ein Textfeld eingeben", "values-are-static-and-defined-manually": "Werte sind statisch und manuell festgelegt", "values-fetched-source-query": "Werte werden über eine Datenquellenabfrage abgerufen", @@ -5832,6 +5833,7 @@ "group-by": "Gruppieren nach", "interval": "Intervall", "query": "Abfrage", + "switch": "", "textbox": "Textfeld" } }, @@ -6176,6 +6178,17 @@ "copy-to-clipboard-failed": "Kopieren in die Zwischenablage fehlgeschlagen" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "Standardwert" }, @@ -10355,7 +10368,7 @@ "title": "Profile" }, "recently-deleted": { - "subtitle": "Alle hier länger als 30 Tage aufgeführten Elemente werden automatisch gelöscht.", + "subtitle": "", "title": "Kürzlich gelöscht" }, "recorded-queries": { @@ -11571,6 +11584,7 @@ "active-jobs": "Aktive Aufträge", "column-action": "Aktion", "column-duration": "Dauer", + "column-job-id": "", "column-message": "Nachricht", "column-started": "Gestartet", "column-status": "Status", @@ -11589,12 +11603,6 @@ "settings": "Einstellungen", "view": "Anzeigen" }, - "repository-health": { - "details": "Details:", - "no-errors-found": "Keine Fehler gefunden", - "title-repository-is-healthy": "Repository in Ordnung", - "title-repository-is-unhealthy": "Repository nicht in Ordnung" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "Branch vergleichen", @@ -11609,7 +11617,7 @@ }, "repository-overview": { "checked": "Geprüft:", - "finished": "Abgeschlossen:", + "finished": "", "health": "Zustand", "healthy": "Guter Zustand", "job-id": "Auftrags-ID:", @@ -11618,7 +11626,6 @@ "not-available": "N/A", "pull-status": "Pull-Status", "resources": "Ressourcen", - "started": "Gestartet:", "status": "Status:", "unhealthy": "Schlechter Zustand", "view-folder": "Ordner anzeigen", @@ -12278,9 +12285,6 @@ "tokens": "Tokens", "tooltip-managed-service-account-cannot-modified": "Dies ist ein verwaltetes Dienstkonto und kann nicht geändert werden" }, - "service-account-permissions": { - "title-permissions": "Berechtigungen" - }, "service-account-profile": { "information": "Informationen", "label-creation-date": "Erstellungsdatum", @@ -12955,6 +12959,10 @@ "name-show-line-numbers": "Zeilennummern anzeigen", "name-show-mini-map": "Minikarte anzeigen" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "Dashboards", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index f839e66622d..2779072b696 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "Añadir un permiso", + "loading": "", "no-permissions": "No hay ningún permiso", "permissions-change-warning": "Esto cambiará los permisos para esta carpeta y todos sus descendientes. En total, esto afectará:", "role": "Rol", "serviceaccount": "Cuenta de servicio", "team": "Equipo", - "title": "Permisos", "user": "Usuario" } }, @@ -3504,7 +3504,7 @@ "delete-button": "Eliminar", "delete-modal-invalid-text": "Una o más carpetas contienen paneles de biblioteca o reglas de alerta. Para continuar, primero elimínelas.", "delete-modal-invalid-title": "No se puede eliminar la carpeta", - "delete-modal-restore-dashboards-text": "Esta acción eliminará las carpetas seleccionadas de inmediato, pero los paneles de control seleccionados se marcarán para su eliminación en 30 días. El administrador de tu organización puede restaurar los paneles de control en cualquier momento antes de que transcurran los 30 días. Las carpetas no se pueden restaurar.", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "Esta acción eliminará el siguiente contenido:", "delete-modal-title": "Eliminar", "delete-provisioned-folder": "Eliminar carpeta aprovisionada", @@ -4572,7 +4572,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "El elemento está oculto debido al renderizado condicional." + "tooltip": "" }, "root": { "title": "Mostrar/ocultar reglas" @@ -5819,6 +5819,7 @@ "dynamically-switch-source-multiple-panels": "Cambiar dinámicamente la fuente de datos para varios paneles", "group": "Añadir claves para agrupar sobre la marcha", "hidden-constant-variable": "Una variable constante oculta, útil para prefijos métricos en dashboards que quieras compartir", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "Los usuarios pueden introducir cualquier cadena arbitraria en un cuadro de texto", "values-are-static-and-defined-manually": "Los valores son estáticos y se definen manualmente", "values-fetched-source-query": "Los valores se obtienen de una consulta de fuente de datos", @@ -5832,6 +5833,7 @@ "group-by": "Agrupar por", "interval": "Intervalo", "query": "Consulta", + "switch": "", "textbox": "Cuadro de texto" } }, @@ -6176,6 +6178,17 @@ "copy-to-clipboard-failed": "Error al copiar al portapapeles" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "Valor predeterminado" }, @@ -10355,7 +10368,7 @@ "title": "Perfiles" }, "recently-deleted": { - "subtitle": "Cualquier elemento que aparezca aquí durante más de 30 días se eliminará automáticamente.", + "subtitle": "", "title": "Eliminados recientemente" }, "recorded-queries": { @@ -11571,6 +11584,7 @@ "active-jobs": "trabajos activos", "column-action": "Acción", "column-duration": "Duración", + "column-job-id": "", "column-message": "Mensaje", "column-started": "Iniciados", "column-status": "Estado", @@ -11589,12 +11603,6 @@ "settings": "Configuración", "view": "Vista" }, - "repository-health": { - "details": "Detalles:", - "no-errors-found": "No se han encontrado errores", - "title-repository-is-healthy": "El repositorio está en buen estado", - "title-repository-is-unhealthy": "El repositorio no está en buen estado" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "Comparar rama", @@ -11609,7 +11617,7 @@ }, "repository-overview": { "checked": "Revisado:", - "finished": "Finalizado:", + "finished": "", "health": "Salud", "healthy": "Lineas", "job-id": "ID del trabajo:", @@ -11618,7 +11626,6 @@ "not-available": "N/D", "pull-status": "Estado de extracción", "resources": "Recursos", - "started": "Inicio:", "status": "Estado:", "unhealthy": "En mal estado", "view-folder": "Ver carpeta", @@ -12278,9 +12285,6 @@ "tokens": "Tokens", "tooltip-managed-service-account-cannot-modified": "Esta es una cuenta de servicio gestionada y no se puede modificar" }, - "service-account-permissions": { - "title-permissions": "Permisos" - }, "service-account-profile": { "information": "Información", "label-creation-date": "Fecha de creación", @@ -12955,6 +12959,10 @@ "name-show-line-numbers": "Mostrar números de línea", "name-show-mini-map": "Mostrar minimapa" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "Paneles de control", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 11ba3782df3..07be7a535f3 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "Ajouter une autorisation", + "loading": "", "no-permissions": "Il n'y a aucune autorisation", "permissions-change-warning": "Cette action modifiera les autorisations pour ce dossier et tous ses descendants. Au total, elle affectera :", "role": "Rôle", "serviceaccount": "Compte de service", "team": "Équipe", - "title": "Autorisations", "user": "Utilisateur" } }, @@ -3504,7 +3504,7 @@ "delete-button": "Supprimer", "delete-modal-invalid-text": "Un ou plusieurs dossiers contiennent des panneaux Bibliothèque ou des règles d'alerte. Vous devez d'abord les supprimer.", "delete-modal-invalid-title": "Impossible de supprimer le dossier", - "delete-modal-restore-dashboards-text": "Cette action supprimera immédiatement les dossiers sélectionnés, mais les tableaux de bord sélectionnés seront marqués pour suppression dans 30 jours. L'administrateur de votre organisation peut restaurer les tableaux de bord à tout moment avant l'expiration des 30 jours. Les dossiers ne peuvent pas être restaurés.", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "Cette action supprimera le contenu suivant :", "delete-modal-title": "Supprimer", "delete-provisioned-folder": "Supprimer le dossier provisionné", @@ -4572,7 +4572,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "L’élément est masqué en raison du rendu conditionnel." + "tooltip": "" }, "root": { "title": "Afficher / masquer les règles" @@ -5819,6 +5819,7 @@ "dynamically-switch-source-multiple-panels": "Changer dynamiquement la source de données pour plusieurs panneaux", "group": "Ajouter des clés de regroupement à la volée", "hidden-constant-variable": "Une variable constante masquée, utile pour les préfixes de métriques dans des tableaux de bord à partager", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "Les utilisateurs peuvent saisir des chaînes de caractères libres dans un champ de texte", "values-are-static-and-defined-manually": "Les valeurs sont statiques et définies manuellement", "values-fetched-source-query": "Les valeurs sont récupérées à partir d’une requête de source de données", @@ -5832,6 +5833,7 @@ "group-by": "Regrouper par", "interval": "Intervalle", "query": "Requête", + "switch": "", "textbox": "Zone de texte" } }, @@ -6176,6 +6178,17 @@ "copy-to-clipboard-failed": "Échec de la copie dans le presse-papiers" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "Valeur par défaut" }, @@ -10355,7 +10368,7 @@ "title": "Profils" }, "recently-deleted": { - "subtitle": "Tous les éléments répertoriés ici depuis plus de 30 jours seront automatiquement supprimés.", + "subtitle": "", "title": "Récemment supprimé" }, "recorded-queries": { @@ -11571,6 +11584,7 @@ "active-jobs": "missions actives", "column-action": "Action", "column-duration": "Durée", + "column-job-id": "", "column-message": "Message", "column-started": "Démarré", "column-status": "Statut", @@ -11589,12 +11603,6 @@ "settings": "Paramètres", "view": "Afficher" }, - "repository-health": { - "details": "Détails :", - "no-errors-found": "Aucune erreur trouvée", - "title-repository-is-healthy": "Le référentiel est en bon état", - "title-repository-is-unhealthy": "Le référentiel est en mauvais état" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "Comparer une branche", @@ -11609,7 +11617,7 @@ }, "repository-overview": { "checked": "Vérifié :", - "finished": "Fini :", + "finished": "", "health": "Santé", "healthy": "Sain", "job-id": "ID de mission :", @@ -11618,7 +11626,6 @@ "not-available": "S/O", "pull-status": "Statut de fusion", "resources": "Ressources", - "started": "Démarré :", "status": "Statut :", "unhealthy": "Non sain", "view-folder": "Afficher le dossier", @@ -12278,9 +12285,6 @@ "tokens": "Jetons", "tooltip-managed-service-account-cannot-modified": "Il s’agit d’un compte de service géré qui ne peut pas être modifié" }, - "service-account-permissions": { - "title-permissions": "Autorisations" - }, "service-account-profile": { "information": "Information", "label-creation-date": "Date de création", @@ -12955,6 +12959,10 @@ "name-show-line-numbers": "Afficher les numéros de ligne", "name-show-mini-map": "Afficher la mini-carte" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "Tableaux de bord", diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 272fdfb1280..68c80aae7d7 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "Engedély hozzáadása", + "loading": "", "no-permissions": "Nincsenek engedélyek", "permissions-change-warning": "Ez módosítja ennek a mappának és minden almappájának az engedélyeit. Összességében ez a következőket érinti:", "role": "Szerepkör", "serviceaccount": "Szolgáltatási fiók", "team": "Csapat", - "title": "Engedélyek", "user": "Felhasználó" } }, @@ -3504,7 +3504,7 @@ "delete-button": "Törlés", "delete-modal-invalid-text": "Egy vagy több mappa könyvtárpaneleket vagy riasztási szabályokat tartalmaz. A folytatáshoz először törölje ezeket.", "delete-modal-invalid-title": "Nem lehet törölni a mappát", - "delete-modal-restore-dashboards-text": "Ez a művelet azonnal törli a kijelölt mappákat, de a kijelölt irányítópultok 30 napon belüli törlésre lesznek megjelölve. A szervezeti rendszergazda a 30 nap lejárta előtt bármikor visszaállíthatja az irányítópultokat. A mappák nem állíthatók vissza.", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "Ez a művelet törli a következő tartalmat:", "delete-modal-title": "Törlés", "delete-provisioned-folder": "Kiépített mappa törlése", @@ -4572,7 +4572,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "Az elem rejtve van a feltételes renderelés miatt." + "tooltip": "" }, "root": { "title": "Szabályok megjelenítése/elrejtése" @@ -5819,6 +5819,7 @@ "dynamically-switch-source-multiple-panels": "Adatforrás dinamikus váltása több panelhez", "group": "Csoportosítási kulcsok hozzáadása menet közben", "hidden-constant-variable": "Rejtett konstans változó, mely hasznos metrikai előtagokhoz a megosztani kívánt irányítópultokon", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "A felhasználók tetszőleges szöveget írhatnak be egy szövegmezőbe", "values-are-static-and-defined-manually": "Az értékek statikusak, és manuálisan vannak megadva", "values-fetched-source-query": "Az értékek egy adatforrás-lekérdezéséből származnak", @@ -5832,6 +5833,7 @@ "group-by": "Csoportosítási szempont", "interval": "Intervallum", "query": "Lekérdezés", + "switch": "", "textbox": "Szövegdoboz" } }, @@ -6176,6 +6178,17 @@ "copy-to-clipboard-failed": "Nem sikerült a vágólapra másolás" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "Alapértelmezett érték" }, @@ -10355,7 +10368,7 @@ "title": "Profilok" }, "recently-deleted": { - "subtitle": "Az itt 30 napnál hosszabb ideig felsorolt elemek automatikusan törlődnek.", + "subtitle": "", "title": "Nemrég törölt" }, "recorded-queries": { @@ -11571,6 +11584,7 @@ "active-jobs": "aktív feladatok", "column-action": "Művelet", "column-duration": "Időtartam", + "column-job-id": "", "column-message": "Üzenet", "column-started": "Elindítva", "column-status": "Állapot", @@ -11589,12 +11603,6 @@ "settings": "Beállítások", "view": "Nézet" }, - "repository-health": { - "details": "Részletek:", - "no-errors-found": "Nem található hiba", - "title-repository-is-healthy": "Az adattár állapota megfelelő", - "title-repository-is-unhealthy": "Az adattár állapota nem megfelelő" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "Ág összehasonlítása", @@ -11609,7 +11617,7 @@ }, "repository-overview": { "checked": "Ellenőrzés:", - "finished": "Befejezés:", + "finished": "", "health": "Állapot", "healthy": "Megfelelő állapotú", "job-id": "Feladatazonosító:", @@ -11618,7 +11626,6 @@ "not-available": "n.a.", "pull-status": "Lekérés állapota", "resources": "Erőforrások", - "started": "Kezdet:", "status": "Állapot:", "unhealthy": "Nem megfelelő állapotú", "view-folder": "Mappa megtekintése", @@ -12278,9 +12285,6 @@ "tokens": "Tokenek", "tooltip-managed-service-account-cannot-modified": "Ez egy felügyelt szolgáltatási fiók, és nem módosítható" }, - "service-account-permissions": { - "title-permissions": "Engedélyek" - }, "service-account-profile": { "information": "Információ", "label-creation-date": "Létrehozás dátuma", @@ -12955,6 +12959,10 @@ "name-show-line-numbers": "Sorszámozás megjelenítése", "name-show-mini-map": "Minitérkép megjelenítése" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "Irányítópultok", diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index 76151588b12..2e42bd349b5 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "Tambahkan izin", + "loading": "", "no-permissions": "Tidak ada izin", "permissions-change-warning": "Ini akan mengubah izin untuk folder ini dan semua turunannya. Secara keseluruhan, ini akan memengaruhi:", "role": "Peran", "serviceaccount": "Akun Layanan", "team": "Tim", - "title": "Izin", "user": "Pengguna" } }, @@ -3492,7 +3492,7 @@ "delete-button": "Hapus", "delete-modal-invalid-text": "Satu atau beberapa folder berisi panel pustaka atau aturan peringatan. Hapus ini terlebih dahulu untuk melanjutkan.", "delete-modal-invalid-title": "Tidak dapat menghapus folder", - "delete-modal-restore-dashboards-text": "Tindakan ini akan segera menghapus folder yang dipilih tetapi dasbor yang dipilih akan ditandai untuk dihapus dalam 30 hari. Administrator organisasi Anda dapat memulihkan dasbor kapan saja sebelum 30 hari kedaluwarsa. Folder tidak dapat dipulihkan.", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "Tindakan ini akan menghapus konten berikut:", "delete-modal-title": "Hapus", "delete-provisioned-folder": "Hapus folder yang disediakan", @@ -4552,7 +4552,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "Elemen disembunyikan karena rendering bersyarat." + "tooltip": "" }, "root": { "title": "Tampilkan/sembunyikan aturan" @@ -5798,6 +5798,7 @@ "dynamically-switch-source-multiple-panels": "Beralih sumber data secara dinamis untuk beberapa panel", "group": "Tambahkan kunci ke grup dengan cepat", "hidden-constant-variable": "Variabel konstan tersembunyi, berguna untuk awalan metrik di dasbor yang ingin Anda bagikan", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "Pengguna dapat memasukkan string acak apa pun dalam kotak teks", "values-are-static-and-defined-manually": "Nilai bersifat statis dan ditentukan secara manual", "values-fetched-source-query": "Nilai diambil dari kueri sumber data", @@ -5811,6 +5812,7 @@ "group-by": "Kelompokkan berdasarkan", "interval": "Interval", "query": "Kueri", + "switch": "", "textbox": "Kotak teks" } }, @@ -6153,6 +6155,17 @@ "copy-to-clipboard-failed": "Gagal menyalin ke papan klip" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "Nilai default" }, @@ -10324,7 +10337,7 @@ "title": "Profil" }, "recently-deleted": { - "subtitle": "Setiap item yang tercantum di sini selama lebih dari 30 hari akan dihapus secara otomatis.", + "subtitle": "", "title": "Baru dihapus" }, "recorded-queries": { @@ -11531,6 +11544,7 @@ "active-jobs": "pekerjaan aktif", "column-action": "Tindakan", "column-duration": "Durasi", + "column-job-id": "", "column-message": "Pesan", "column-started": "Dimulai", "column-status": "Status", @@ -11549,12 +11563,6 @@ "settings": "Pengaturan", "view": "Lihat" }, - "repository-health": { - "details": "Detail:", - "no-errors-found": "Tidak ada kesalahan yang ditemukan", - "title-repository-is-healthy": "Repositori sehat", - "title-repository-is-unhealthy": "Repositori tidak sehat" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "Bandingkan cabang", @@ -11569,7 +11577,7 @@ }, "repository-overview": { "checked": "Diperiksa:", - "finished": "Selesai:", + "finished": "", "health": "Kesehatan", "healthy": "Sehat", "job-id": "ID Pekerjaan:", @@ -11578,7 +11586,6 @@ "not-available": "N/A", "pull-status": "Status pull", "resources": "Sumber Daya", - "started": "Dimulai:", "status": "Status:", "unhealthy": "Tidak Sehat", "view-folder": "Lihat Folder", @@ -12235,9 +12242,6 @@ "tokens": "Token", "tooltip-managed-service-account-cannot-modified": "Ini adalah akun layanan terkelola dan tidak dapat dimodifikasi" }, - "service-account-permissions": { - "title-permissions": "Izin" - }, "service-account-profile": { "information": "Informasi", "label-creation-date": "Tanggal pembuatan", @@ -12910,6 +12914,10 @@ "name-show-line-numbers": "Tampilkan nomor baris", "name-show-mini-map": "Tampilkan peta mini" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "Dasbor", diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 70dc218a0bc..bd1b1e48532 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "Aggiungi un'autorizzazione", + "loading": "", "no-permissions": "Non sono presenti autorizzazioni", "permissions-change-warning": "Questa operazione modificherà le autorizzazioni per questa cartella e tutti i suoi discendenti. In totale, questo influenzerà:", "role": "Ruolo", "serviceaccount": "Account del servizio", "team": "Team", - "title": "Autorizzazioni", "user": "Utente" } }, @@ -3504,7 +3504,7 @@ "delete-button": "Elimina", "delete-modal-invalid-text": "Una o più cartelle contengono pannelli della libreria o regole di avviso. Eliminarli prima di procedere.", "delete-modal-invalid-title": "Impossibile eliminare la cartella", - "delete-modal-restore-dashboards-text": "Questa azione eliminerà immediatamente le cartelle selezionate, ma i dashboard selezionati verranno contrassegnati per l'eliminazione tra 30 giorni. L'amministratore dell'organizzazione può ripristinare i dashboard in qualsiasi momento prima della scadenza dei 30 giorni. Non è possibile ripristinare le cartelle.", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "Questa azione eliminerà il seguente contenuto:", "delete-modal-title": "Elimina", "delete-provisioned-folder": "Elimina cartella fornita", @@ -4572,7 +4572,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "L'elemento è nascosto a causa del rendering condizionale." + "tooltip": "" }, "root": { "title": "Mostra/nascondi regole" @@ -5819,6 +5819,7 @@ "dynamically-switch-source-multiple-panels": "Cambia dinamicamente l'origine dati per più pannelli", "group": "Aggiungi chiavi per raggruppare al volo", "hidden-constant-variable": "Una variabile costante nascosta, utile per i prefissi metrici nelle dashboard che desideri condividere", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "Gli utenti possono inserire qualsiasi stringa arbitraria in una casella di testo", "values-are-static-and-defined-manually": "I valori sono statici e definiti manualmente", "values-fetched-source-query": "I valori vengono recuperati da una query dell'origine dati", @@ -5832,6 +5833,7 @@ "group-by": "Raggruppa per", "interval": "Intervallo", "query": "Query", + "switch": "", "textbox": "Casella di testo" } }, @@ -6176,6 +6178,17 @@ "copy-to-clipboard-failed": "Copia negli appunti non riuscita" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "Valore predefinito" }, @@ -10355,7 +10368,7 @@ "title": "Profili" }, "recently-deleted": { - "subtitle": "Tutti gli elementi elencati qui per più di 30 giorni verranno eliminati automaticamente.", + "subtitle": "", "title": "Eliminati di recente" }, "recorded-queries": { @@ -11571,6 +11584,7 @@ "active-jobs": "attività attive", "column-action": "Azione", "column-duration": "Durata", + "column-job-id": "", "column-message": "Messaggio", "column-started": "Iniziata", "column-status": "Stato", @@ -11589,12 +11603,6 @@ "settings": "Impostazioni", "view": "Visualizza" }, - "repository-health": { - "details": "Dettagli:", - "no-errors-found": "Nessun errore trovato", - "title-repository-is-healthy": "Il repository è integro", - "title-repository-is-unhealthy": "Il repository non è integro" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "Confronta settore", @@ -11609,7 +11617,7 @@ }, "repository-overview": { "checked": "Verificato:", - "finished": "Terminato:", + "finished": "", "health": "Stato", "healthy": "Correttamente funzionante", "job-id": "ID attività:", @@ -11618,7 +11626,6 @@ "not-available": "N/D", "pull-status": "Stato del pull", "resources": "Risorse", - "started": "Iniziato:", "status": "Stato:", "unhealthy": "Malfunzionante", "view-folder": "Visualizza cartella", @@ -12278,9 +12285,6 @@ "tokens": "Token", "tooltip-managed-service-account-cannot-modified": "Questo è un account di servizio gestito e non può essere modificato" }, - "service-account-permissions": { - "title-permissions": "Autorizzazioni" - }, "service-account-profile": { "information": "Informazioni", "label-creation-date": "Data di creazione", @@ -12955,6 +12959,10 @@ "name-show-line-numbers": "Mostra numeri di riga", "name-show-mini-map": "Mostra mini mappa" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "Dashboard", diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index aa633bbf3ad..ff9721e1255 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "権限を追加する", + "loading": "", "no-permissions": "権限がありません", "permissions-change-warning": "これにより、このフォルダとそのすべての子孫のアクセス許可が変更されます。合計で、これは次のように影響します:", "role": "役割", "serviceaccount": "サービスアカウント", "team": "チーム", - "title": "権限", "user": "ユーザー" } }, @@ -3492,7 +3492,7 @@ "delete-button": "削除", "delete-modal-invalid-text": "1つ以上のフォルダにライブラリパネルまたはアラートルールが含まれています。続行するには、まずこれらを削除してください。", "delete-modal-invalid-title": "フォルダを削除できません", - "delete-modal-restore-dashboards-text": "このアクションにより、選択したフォルダはすぐに削除されますが、選択したダッシュボードは30日後に削除されます。組織の管理者は、30日が経過する前であればいつでもダッシュボードを復元できます。フォルダは復元できません。", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "この操作により、次のコンテンツが削除されます:", "delete-modal-title": "削除", "delete-provisioned-folder": "プロビジョニングされたフォルダを削除", @@ -4552,7 +4552,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "条件付きレンダリング設定のため、要素は非表示になっています。" + "tooltip": "" }, "root": { "title": "表示/非表示ルール" @@ -5798,6 +5798,7 @@ "dynamically-switch-source-multiple-panels": "複数のパネルのデータソースを動的に切り替える", "group": "即時にグループ化キーを追加", "hidden-constant-variable": "共有するダッシュボードのメトリックプレフィックスに便利な非表示の定数変数", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "ユーザーはテキストボックスに任意の文字列を入力できます", "values-are-static-and-defined-manually": "値はスタティックであり、手動で定義されます", "values-fetched-source-query": "値はデータソースクエリから取得されます", @@ -5811,6 +5812,7 @@ "group-by": "グループ化", "interval": "間隔", "query": "クエリ", + "switch": "", "textbox": "テキストボックス" } }, @@ -6153,6 +6155,17 @@ "copy-to-clipboard-failed": "クリップボードへのコピーが失敗しました" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "既定値" }, @@ -10324,7 +10337,7 @@ "title": "プロフィール" }, "recently-deleted": { - "subtitle": "ここに30日以上リストされているアイテムはすべて自動的に削除されます。", + "subtitle": "", "title": "最近削除された" }, "recorded-queries": { @@ -11531,6 +11544,7 @@ "active-jobs": "アクティブなジョブ", "column-action": "操作", "column-duration": "継続時間", + "column-job-id": "", "column-message": "メッセージ", "column-started": "開始済み", "column-status": "ステータス", @@ -11549,12 +11563,6 @@ "settings": "設定", "view": "表示" }, - "repository-health": { - "details": "詳細:", - "no-errors-found": "エラーは見つかりませんでした", - "title-repository-is-healthy": "リポジトリは正常です", - "title-repository-is-unhealthy": "リポジトリに問題があります" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "ブランチを比較", @@ -11569,7 +11577,7 @@ }, "repository-overview": { "checked": "確認済み:", - "finished": "終了:", + "finished": "", "health": "健康", "healthy": "正常", "job-id": "ジョブID:", @@ -11578,7 +11586,6 @@ "not-available": "該当なし", "pull-status": "プル状態", "resources": "リソース", - "started": "開始:", "status": "ステータス:", "unhealthy": "異常", "view-folder": "フォルダを表示", @@ -12235,9 +12242,6 @@ "tokens": "トークン", "tooltip-managed-service-account-cannot-modified": "これは管理対象サービスアカウントであり、変更できません" }, - "service-account-permissions": { - "title-permissions": "権限" - }, "service-account-profile": { "information": "情報", "label-creation-date": "作成日", @@ -12910,6 +12914,10 @@ "name-show-line-numbers": "行番号を表示", "name-show-mini-map": "ミニマップを表示" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "ダッシュボード", diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 8c22a2d056f..f7ad0f896c4 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "권한 추가", + "loading": "", "no-permissions": "권한 없음", "permissions-change-warning": "이 작업을 수행하면 이 폴더와 모든 하위 폴더에 대한 권한이 변경됩니다. 이는 전체적으로 다음에 영향을 미칩니다.", "role": "역할", "serviceaccount": "서비스 계정", "team": "팀", - "title": "권한", "user": "사용자" } }, @@ -3492,7 +3492,7 @@ "delete-button": "삭제", "delete-modal-invalid-text": "하나 이상의 폴더에 라이브러리 패널 또는 경고 규칙이 포함되어 있습니다. 계속하려면 먼저 이 항목들을 삭제하세요.", "delete-modal-invalid-title": "폴더를 삭제할 수 없음", - "delete-modal-restore-dashboards-text": "이 작업을 수행하면 선택한 폴더가 즉시 삭제되지만 선택한 대시보드는 30일 후에 삭제 대상으로 표시됩니다. 조직 관리자는 30일이 만료되기 전에 언제든지 대시보드를 복구할 수 있습니다. 폴더를 복구할 수 없습니다.", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "이 작업을 수행하면 다음 콘텐츠가 삭제됩니다.", "delete-modal-title": "삭제", "delete-provisioned-folder": "프로비저닝된 폴더 삭제", @@ -4552,7 +4552,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "조건부 렌더링으로 인해 요소가 숨겨져 있습니다." + "tooltip": "" }, "root": { "title": "규칙 표시 / 숨기기" @@ -5798,6 +5798,7 @@ "dynamically-switch-source-multiple-panels": "여러 패널의 데이터 소스를 동적으로 전환합니다", "group": "실시간으로 그룹화에 사용할 키를 추가합니다", "hidden-constant-variable": "공유하려는 대시보드의 메트릭 접두사에 유용한, 숨겨진 상수형 변수", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "사용자는 텍스트 상자에 임의의 문자열을 입력할 수 있습니다", "values-are-static-and-defined-manually": "값은 정적이며 수동으로 정의됩니다", "values-fetched-source-query": "값은 데이터 소스 쿼리에서 가져옵니다", @@ -5811,6 +5812,7 @@ "group-by": "그룹화 기준", "interval": "간격", "query": "쿼리", + "switch": "", "textbox": "텍스트 상자" } }, @@ -6153,6 +6155,17 @@ "copy-to-clipboard-failed": "클립보드로 복사 실패" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "기본값" }, @@ -10324,7 +10337,7 @@ "title": "프로필" }, "recently-deleted": { - "subtitle": "여기에 30일 이상 기재된 모든 항목이 자동으로 삭제됩니다.", + "subtitle": "", "title": "최근에 삭제됨" }, "recorded-queries": { @@ -11531,6 +11544,7 @@ "active-jobs": "활성 작업", "column-action": "동작", "column-duration": "지속 시간", + "column-job-id": "", "column-message": "메시지", "column-started": "시작됨", "column-status": "상태", @@ -11549,12 +11563,6 @@ "settings": "설정", "view": "보기" }, - "repository-health": { - "details": "세부 정보:", - "no-errors-found": "오류를 찾을 수 없습니다", - "title-repository-is-healthy": "리포지토리 상태가 좋습니다", - "title-repository-is-unhealthy": "리포지토리 상태가 좋지 않습니다" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "브랜치 비교", @@ -11569,7 +11577,7 @@ }, "repository-overview": { "checked": "확인됨:", - "finished": "완료됨:", + "finished": "", "health": "상태", "healthy": "좋은 상태", "job-id": "작업 ID:", @@ -11578,7 +11586,6 @@ "not-available": "해당 없음", "pull-status": "풀 상태", "resources": "리소스", - "started": "시작됨:", "status": "상태:", "unhealthy": "좋지 않은 상태", "view-folder": "폴더 보기", @@ -12235,9 +12242,6 @@ "tokens": "토큰", "tooltip-managed-service-account-cannot-modified": "관리되는 서비스 계정이므로 수정할 수 없습니다" }, - "service-account-permissions": { - "title-permissions": "권한" - }, "service-account-profile": { "information": "정보", "label-creation-date": "생성일", @@ -12910,6 +12914,10 @@ "name-show-line-numbers": " 번호 표시", "name-show-mini-map": "미니 지도 표시" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "대시보드", diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 6210cb1355e..b1c96c42c8b 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "Een toestemming toevoegen", + "loading": "", "no-permissions": "Er zijn geen toestemmingen", "permissions-change-warning": "Dit wijzigt de toestemmingen voor deze map en alle onderliggende mappen. In totaal heeft dit invloed op:", "role": "Rol", "serviceaccount": "Serviceaccount", "team": "Team", - "title": "Toestemmingen", "user": "Gebruiker" } }, @@ -3504,7 +3504,7 @@ "delete-button": "Verwijderen", "delete-modal-invalid-text": "Een of meer mappen bevatten bibliotheekpanelen of waarschuwingsregels. Verwijder deze eerst om verder te gaan.", "delete-modal-invalid-title": "Kan de map niet verwijderen", - "delete-modal-restore-dashboards-text": "Deze actie verwijdert de geselecteerde mappen onmiddellijk, maar de geselecteerde dashboards worden gemarkeerd voor verwijdering over 30 dagen. Je organisatiebeheerder kan de dashboards op elk moment herstellen voordat de 30 dagen verlopen. Mappen kunnen niet worden hersteld.", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "Met deze actie wordt de volgende inhoud verwijderd:", "delete-modal-title": "Verwijderen", "delete-provisioned-folder": "Geprovisioneerde map verwijderen", @@ -4572,7 +4572,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "Element is verborgen vanwege voorwaardelijke weergave." + "tooltip": "" }, "root": { "title": "Regels tonen / verbergen" @@ -5819,6 +5819,7 @@ "dynamically-switch-source-multiple-panels": "Schakel dynamisch tussen gegevensbronnen voor meerdere panelen", "group": "Voeg direct sleutels toe om te groeperen", "hidden-constant-variable": "Een verborgen constante variabele, handig voor metrische voorvoegsels in dashboards die je wilt delen", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "Gebruikers kunnen willekeurige strings invoeren in een tekstvak", "values-are-static-and-defined-manually": "Waarden zijn statisch en handmatig gedefinieerd", "values-fetched-source-query": "Waarden worden opgehaald uit een gegevensbronquery", @@ -5832,6 +5833,7 @@ "group-by": "Groeperen op", "interval": "Interval", "query": "Query", + "switch": "", "textbox": "Tekstvak" } }, @@ -6176,6 +6178,17 @@ "copy-to-clipboard-failed": "Kopiëren naar klembord mislukt" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "Standaardwaarde" }, @@ -10355,7 +10368,7 @@ "title": "Profielen" }, "recently-deleted": { - "subtitle": "Alle items die hier langer dan 30 dagen worden vermeld, worden automatisch verwijderd.", + "subtitle": "", "title": "Onlangs verwijderd" }, "recorded-queries": { @@ -11571,6 +11584,7 @@ "active-jobs": "actieve taken", "column-action": "Actie", "column-duration": "Duur", + "column-job-id": "", "column-message": "Bericht", "column-started": "Gestart", "column-status": "Status", @@ -11589,12 +11603,6 @@ "settings": "Instellingen", "view": "Weergave" }, - "repository-health": { - "details": "Details:", - "no-errors-found": "Geen fouten gevonden", - "title-repository-is-healthy": "Repository is gezond", - "title-repository-is-unhealthy": "Repository is ongezond" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "Tak vergelijken", @@ -11609,7 +11617,7 @@ }, "repository-overview": { "checked": "Gecontroleerd:", - "finished": "Beëindigd:", + "finished": "", "health": "Gezondheid", "healthy": "Gezond", "job-id": "Taak-id:", @@ -11618,7 +11626,6 @@ "not-available": "N.v.t.", "pull-status": "Pull-status", "resources": "Bronnen", - "started": "Begonnen:", "status": "Status:", "unhealthy": "Ongezond", "view-folder": "Map bekijken", @@ -12278,9 +12285,6 @@ "tokens": "Tokens", "tooltip-managed-service-account-cannot-modified": "Dit is een beheerd serviceaccount dat niet kan worden gewijzigd" }, - "service-account-permissions": { - "title-permissions": "Toestemmingen" - }, "service-account-profile": { "information": "Informatie", "label-creation-date": "Aanmaakdatum", @@ -12955,6 +12959,10 @@ "name-show-line-numbers": "Regelnummers tonen", "name-show-mini-map": "Minikaart weergeven" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "Dashboards", diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index f7ef9d2d7af..5ba95448729 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "Dodaj uprawnienie", + "loading": "", "no-permissions": "Brak uprawnień", "permissions-change-warning": "Spowoduje to zmianę uprawnień dla tego folderu i elementów po nim dziedziczących. Łącznie wpłynie to na następujące elementy:", "role": "Rola", "serviceaccount": "Konto usługi", "team": "Zespół", - "title": "Uprawnienia", "user": "Użytkownik" } }, @@ -3528,7 +3528,7 @@ "delete-button": "Usuń", "delete-modal-invalid-text": "Co najmniej jeden folder zawiera panele bibliotek lub reguły alertów. Usuń je, aby kontynuować.", "delete-modal-invalid-title": "Nie można usunąć folderu", - "delete-modal-restore-dashboards-text": "To działanie spowoduje natychmiastowe usunięcie wybranych folderów, ale wybrane pulpity zostaną oznaczone do usunięcia za 30 dni. Administrator organizacji może przywrócić pulpity w dowolnym momencie przed upływem 30 dni. Nie można przywrócić folderów.", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "To działanie spowoduje usunięcie następującej zawartości:", "delete-modal-title": "Usuń", "delete-provisioned-folder": "Usuń przydzielony folder", @@ -4612,7 +4612,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "Element jest ukryty z powodu renderowania warunkowego." + "tooltip": "" }, "root": { "title": "Pokaż/ukryj reguły" @@ -5861,6 +5861,7 @@ "dynamically-switch-source-multiple-panels": "Dynamiczne przełączanie źródła danych dla wielu paneli", "group": "Dodaj klucze do grupowania w czasie rzeczywistym", "hidden-constant-variable": "Ukryta zmienna stała, przydatna do prefiksów metrycznych w przypadku pulpitów, które chcesz udostępnić", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "Użytkownicy mogą wprowadzać dowolne ciągi w polu tekstowym", "values-are-static-and-defined-manually": "Wartości są statyczne i definiowane ręcznie", "values-fetched-source-query": "Wartości są pobierane z zapytania dotyczącego źródła danych", @@ -5874,6 +5875,7 @@ "group-by": "Grupuj według", "interval": "Odstęp czasu", "query": "Zapytanie", + "switch": "", "textbox": "Pole tekstowe" } }, @@ -6222,6 +6224,17 @@ "copy-to-clipboard-failed": "Nie udało się skopiować do schowka" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "Domyślna wartość" }, @@ -10417,7 +10430,7 @@ "title": "Profile" }, "recently-deleted": { - "subtitle": "Wszystkie pozycje wypisane tutaj przez ponad 30 dni zostaną automatycznie usunięte.", + "subtitle": "", "title": "Ostatnio usunięte" }, "recorded-queries": { @@ -11651,6 +11664,7 @@ "active-jobs": "aktywne zadania", "column-action": "Działanie", "column-duration": "Czas trwania", + "column-job-id": "", "column-message": "Wiadomość", "column-started": "Rozpoczęto", "column-status": "Status", @@ -11669,12 +11683,6 @@ "settings": "Ustawienia", "view": "Wyświetl" }, - "repository-health": { - "details": "Szczegóły:", - "no-errors-found": "Nie znaleziono błędów", - "title-repository-is-healthy": "Repozytorium jest sprawne", - "title-repository-is-unhealthy": "Repozytorium jest niesprawne" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "Porównaj gałąź", @@ -11689,7 +11697,7 @@ }, "repository-overview": { "checked": "Sprawdzone:", - "finished": "Zakończone:", + "finished": "", "health": "Zdrowie", "healthy": "Sprawne", "job-id": "Identyfikator zadania:", @@ -11698,7 +11706,6 @@ "not-available": "Niedostępne", "pull-status": "Status pull", "resources": "Zasoby", - "started": "Rozpoczęte:", "status": "Status:", "unhealthy": "Niesprawne", "view-folder": "Wyświetl folder", @@ -12364,9 +12371,6 @@ "tokens": "Tokeny", "tooltip-managed-service-account-cannot-modified": "To jest zarządzane konto usługi i nie można go zmodyfikować" }, - "service-account-permissions": { - "title-permissions": "Uprawnienia" - }, "service-account-profile": { "information": "Informacje", "label-creation-date": "Data utworzenia", @@ -13045,6 +13049,10 @@ "name-show-line-numbers": "Pokaż numerację wierszy", "name-show-mini-map": "Pokaż minimapę" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "Pulpity", diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 1aa3306ea04..1f4d97d21f4 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "Adicionar uma permissão", + "loading": "", "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": "Conta de serviço", "team": "Equipe", - "title": "Permissões", "user": "Usuário" } }, @@ -3504,7 +3504,7 @@ "delete-button": "Excluir", "delete-modal-invalid-text": "Uma ou mais pastas contêm painéis de biblioteca, ou regras de alerta. Exclua-as primeiro para prosseguir.", "delete-modal-invalid-title": "Não é possível excluir pasta", - "delete-modal-restore-dashboards-text": "Esta ação excluirá as pastas selecionadas imediatamente, mas os painéis de controle selecionados serão marcados para exclusão em 30 dias. O administrador da sua organização pode restaurar os painéis de controle a qualquer momento antes do término do prazo de 30 dias. As pastas não podem ser restauradas.", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "Esta ação apagará o seguinte conteúdo:", "delete-modal-title": "Excluir", "delete-provisioned-folder": "Excluir pasta provisionada", @@ -4572,7 +4572,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "O elemento está oculto devido à renderização condicional." + "tooltip": "" }, "root": { "title": "Exibir/Ocultar regras" @@ -5819,6 +5819,7 @@ "dynamically-switch-source-multiple-panels": "Alternar dinamicamente a fonte de dados para vários painéis", "group": "Adicionar chaves para agrupar em tempo real", "hidden-constant-variable": "Uma variável constante oculta, útil para prefixos métricos em painéis de controle que você deseja compartilhar", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "Os usuários podem inserir qualquer string arbitrária em uma caixa de texto", "values-are-static-and-defined-manually": "Os valores são estáticos e definidos manualmente", "values-fetched-source-query": "Os valores são obtidos a partir de uma consulta de fonte de dados", @@ -5832,6 +5833,7 @@ "group-by": "Agrupar por", "interval": "Intervalo", "query": "Consulta", + "switch": "", "textbox": "Caixa de texto" } }, @@ -6176,6 +6178,17 @@ "copy-to-clipboard-failed": "Falha ao copiar para a área de transferência" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "Valor padrão" }, @@ -10355,7 +10368,7 @@ "title": "Perfis" }, "recently-deleted": { - "subtitle": "Todos os itens listados aqui por mais de 30 dias serão excluídos automaticamente.", + "subtitle": "", "title": "Excluídos recentemente" }, "recorded-queries": { @@ -11571,6 +11584,7 @@ "active-jobs": "tarefas ativas", "column-action": "Ação", "column-duration": "Duração", + "column-job-id": "", "column-message": "Mensagem", "column-started": "Iniciado", "column-status": "Status", @@ -11589,12 +11603,6 @@ "settings": "Configurações", "view": "Visualizar" }, - "repository-health": { - "details": "Detalhes:", - "no-errors-found": "Nenhum erro encontrado", - "title-repository-is-healthy": "O repositório está estável", - "title-repository-is-unhealthy": "O repositório está instável" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "Comparar branch", @@ -11609,7 +11617,7 @@ }, "repository-overview": { "checked": "Verificado:", - "finished": "Finalizado:", + "finished": "", "health": "Integridade", "healthy": "Estável", "job-id": "ID da tarefa:", @@ -11618,7 +11626,6 @@ "not-available": "Não se aplica", "pull-status": "Status da extração", "resources": "Fontes", - "started": "Iniciado:", "status": "Status:", "unhealthy": "Instável", "view-folder": "Visualizar pasta", @@ -12278,9 +12285,6 @@ "tokens": "Tokens", "tooltip-managed-service-account-cannot-modified": "Esta é uma conta de serviço gerenciada e não pode ser modificada" }, - "service-account-permissions": { - "title-permissions": "Permissões" - }, "service-account-profile": { "information": "Informações", "label-creation-date": "Data de criação", @@ -12955,6 +12959,10 @@ "name-show-line-numbers": "Exibir números das linhas", "name-show-mini-map": "Mostrar minimapa" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "Painéis de controle", diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 3521ee0e58d..aca99fc50ea 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "Adicionar uma permissão", + "loading": "", "no-permissions": "Não existem permissões", "permissions-change-warning": "Isto alterará as permissões para esta pasta e todas as suas descendentes. No total, isto afetará:", "role": "Função", "serviceaccount": "Conta de serviços", "team": "Equipa", - "title": "Permissões", "user": "Utilizador" } }, @@ -3504,7 +3504,7 @@ "delete-button": "Eliminar", "delete-modal-invalid-text": "Uma ou mais pastas contêm painéis de biblioteca ou regras de alerta. Elimine-as primeiro para continuar.", "delete-modal-invalid-title": "Não é possível eliminar a pasta", - "delete-modal-restore-dashboards-text": "Esta ação eliminará as pastas selecionadas imediatamente, mas os painéis selecionados serão marcados para eliminação em 30 dias. O administrador da sua organização pode restaurar os painéis a qualquer momento antes que os 30 dias expirem. Não é possível restaurar as pastas.", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "Esta ação eliminará o seguinte conteúdo:", "delete-modal-title": "Eliminar", "delete-provisioned-folder": "Eliminar a pasta disponibilizada", @@ -4572,7 +4572,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "O elemento está oculto devido à renderização condicional." + "tooltip": "" }, "root": { "title": "Mostrar/ocultar regras" @@ -5819,6 +5819,7 @@ "dynamically-switch-source-multiple-panels": "Alternar dinamicamente a origem de dados para vários painéis", "group": "Adicionar chaves para agrupar em tempo real", "hidden-constant-variable": "Uma variável constante oculta, útil para prefixos métricos em painéis que pretende partilhar", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "Os utilizadores podem inserir quaisquer cadeias de caracteres arbitrárias numa caixa de texto", "values-are-static-and-defined-manually": "Os valores são estáticos e definidos manualmente", "values-fetched-source-query": "Os valores são obtidos a partir de uma consulta de origem de dados", @@ -5832,6 +5833,7 @@ "group-by": "Agrupar por", "interval": "Intervalo", "query": "Consulta", + "switch": "", "textbox": "Caixa de texto" } }, @@ -6176,6 +6178,17 @@ "copy-to-clipboard-failed": "A cópia para a área de transferência falhou" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "Valor predefinido" }, @@ -10355,7 +10368,7 @@ "title": "Profiles" }, "recently-deleted": { - "subtitle": "Quaisquer elementos listados aqui por mais de 30 dias serão eliminados automaticamente.", + "subtitle": "", "title": "Eliminado recentemente" }, "recorded-queries": { @@ -11571,6 +11584,7 @@ "active-jobs": "trabalhos ativos", "column-action": "Ação", "column-duration": "Duração", + "column-job-id": "", "column-message": "Mensagem", "column-started": "Iniciado", "column-status": "Estado", @@ -11589,12 +11603,6 @@ "settings": "Definições", "view": "Ver" }, - "repository-health": { - "details": "Detalhes:", - "no-errors-found": "Nenhum erro encontrado", - "title-repository-is-healthy": "O repositório está em bom estado", - "title-repository-is-unhealthy": "O repositório não está em bom estado" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "Comparar filial", @@ -11609,7 +11617,7 @@ }, "repository-overview": { "checked": "Verificado:", - "finished": "Concluído:", + "finished": "", "health": "Saúde", "healthy": "Em bom estado", "job-id": "ID do trabalho:", @@ -11618,7 +11626,6 @@ "not-available": "N/A", "pull-status": "Estado da extração", "resources": "Recursos", - "started": "Iniciado:", "status": "Estado:", "unhealthy": "Não em bom estado", "view-folder": "Visualizar pasta", @@ -12278,9 +12285,6 @@ "tokens": "Tokens", "tooltip-managed-service-account-cannot-modified": "Esta é uma conta de serviço gerida e não pode ser modificada" }, - "service-account-permissions": { - "title-permissions": "Permissões" - }, "service-account-profile": { "information": "Informação", "label-creation-date": "Data de criação", @@ -12955,6 +12959,10 @@ "name-show-line-numbers": "Mostrar os números das linhas", "name-show-mini-map": "Mostrar o minimapa" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "Painéis de controlo", diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index 6ada3fb8075..c6bd50cb927 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "Добавить разрешение", + "loading": "", "no-permissions": "Нет разрешений", "permissions-change-warning": "Изменятся разрешения для этой и всех дочерних папок. В итоге будут затронуты:", "role": "Роль", "serviceaccount": "Служебная учетная запись", "team": "Команда", - "title": "Разрешения", "user": "Пользователь" } }, @@ -3528,7 +3528,7 @@ "delete-button": "Удалить", "delete-modal-invalid-text": "Одна или несколько папок содержат панели библиотеки или правила оповещения. Чтобы продолжить, сначала удалите их.", "delete-modal-invalid-title": "Невозможно удалить папку", - "delete-modal-restore-dashboards-text": "Это действие приведет к немедленному удалению выбранных папок, а выбранные дашборды будут помечены для удаления через 30 дней. Администратор вашей организации может восстановить дашборды в любое время в течение указанных 30 дней. Папки нельзя восстановить.", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "Это действие приведет к удалению следующего контента:", "delete-modal-title": "Удаление", "delete-provisioned-folder": "Удалить подготовленную папку", @@ -4612,7 +4612,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "Элемент скрыт из-за условной визуализации." + "tooltip": "" }, "root": { "title": "Отображение/скрытие правил" @@ -5861,6 +5861,7 @@ "dynamically-switch-source-multiple-panels": "Динамическое переключение источника данных для нескольких панелей", "group": "Добавляйте ключи для группировки в процессе работы", "hidden-constant-variable": "Скрытая константная переменная для префиксов метрик на дашбордах, доступ к которым вы хотите открыть", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "Пользователи могут вводить любые произвольные строки в текстовое поле", "values-are-static-and-defined-manually": "Значения являются статическими и задаются вручную", "values-fetched-source-query": "Значения извлекаются из запроса к источнику данных", @@ -5874,6 +5875,7 @@ "group-by": "Группировать по", "interval": "Интервал", "query": "Запрос", + "switch": "", "textbox": "Текстовое поле" } }, @@ -6222,6 +6224,17 @@ "copy-to-clipboard-failed": "Не удалось скопировать в буфер обмена" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "Значение по умолчанию" }, @@ -10417,7 +10430,7 @@ "title": "Профили" }, "recently-deleted": { - "subtitle": "Все перечисленные элементы по истечении 30 дней будут автоматически удалены", + "subtitle": "", "title": "Последние удаленные" }, "recorded-queries": { @@ -11651,6 +11664,7 @@ "active-jobs": "активные задания", "column-action": "Действие", "column-duration": "Длительность", + "column-job-id": "", "column-message": "Сообщение", "column-started": "Запущено", "column-status": "Статус", @@ -11669,12 +11683,6 @@ "settings": "Параметры", "view": "Просмотр" }, - "repository-health": { - "details": "Сведения:", - "no-errors-found": "Ошибок не обнаружено", - "title-repository-is-healthy": "Репозиторий исправен", - "title-repository-is-unhealthy": "Репозиторий неисправен" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "Сравнить ветвь", @@ -11689,7 +11697,7 @@ }, "repository-overview": { "checked": "Проверено:", - "finished": "Завершено:", + "finished": "", "health": "Работоспособность", "healthy": "Исправен", "job-id": "Идентификатор задания:", @@ -11698,7 +11706,6 @@ "not-available": "Н/Д", "pull-status": "Состояние включения изменений", "resources": "Ресурсы", - "started": "Запущено:", "status": "Состояние:", "unhealthy": "Неисправен", "view-folder": "Просмотр папки", @@ -12364,9 +12371,6 @@ "tokens": "Токены", "tooltip-managed-service-account-cannot-modified": "Это управляемая служебная учетная запись, которую невозможно изменить" }, - "service-account-permissions": { - "title-permissions": "Разрешения" - }, "service-account-profile": { "information": "Информация", "label-creation-date": "Дата создания", @@ -13045,6 +13049,10 @@ "name-show-line-numbers": "Показывать номера строк", "name-show-mini-map": "Показать мини-карту" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "Дашборды", diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 65882cc56df..128f3559270 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "Lägg till en behörighet", + "loading": "", "no-permissions": "Det finns inga behörigheter", "permissions-change-warning": "Detta kommer att ändra behörigheter för den här mappen och alla dess underordnade. Totalt kommer detta att påverka:", "role": "Roll", "serviceaccount": "Servicekonto", "team": "Team", - "title": "Behörigheter", "user": "Användare" } }, @@ -3504,7 +3504,7 @@ "delete-button": "Ta bort", "delete-modal-invalid-text": "En eller flera mappar innehåller bibliotekspaneler eller varningsregler. Ta först bort dessa innan du fortsätter.", "delete-modal-invalid-title": "Det går inte att ta bort mapp", - "delete-modal-restore-dashboards-text": "Denna åtgärd raderar de valda mapparna omedelbart, men de valda instrumentpanelerna markeras för radering om 30 dagar. Din organisationsadministratör kan återställa instrumentpanelerna när som helst innan de 30 dagarna löper ut. Mappar kan inte återställas.", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "Denna åtgärd raderar följande innehåll:", "delete-modal-title": "Ta bort", "delete-provisioned-folder": "Ta bort provisionerad mapp", @@ -4572,7 +4572,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "Elementet är dolt på grund av villkorsstyrd rendering." + "tooltip": "" }, "root": { "title": "Visa/dölj regler" @@ -5819,6 +5819,7 @@ "dynamically-switch-source-multiple-panels": "Byt datakälla dynamiskt för flera paneler", "group": "Lägg till nycklar att gruppera efter i farten", "hidden-constant-variable": "En dold konstant variabel, användbar för metriska prefix i kontrollpaneler som du vill dela", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "Användare kan ange valfria strängar i en textruta", "values-are-static-and-defined-manually": "Värdena är statiska och definieras manuellt", "values-fetched-source-query": "Värden hämtas från en datakällfråga", @@ -5832,6 +5833,7 @@ "group-by": "Gruppera efter", "interval": "Intervall", "query": "Fråga", + "switch": "", "textbox": "Textfält" } }, @@ -6176,6 +6178,17 @@ "copy-to-clipboard-failed": "Kopiering till urklipp misslyckades" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "Standardvärde" }, @@ -10355,7 +10368,7 @@ "title": "Profiler" }, "recently-deleted": { - "subtitle": "Alla objekt som listas här i fler än 30 dagar raderas automatiskt.", + "subtitle": "", "title": "Nyligen raderat" }, "recorded-queries": { @@ -11571,6 +11584,7 @@ "active-jobs": "aktiva jobb", "column-action": "Åtgärd", "column-duration": "Varaktighet", + "column-job-id": "", "column-message": "Meddelande", "column-started": "Startat", "column-status": "Status", @@ -11589,12 +11603,6 @@ "settings": "Inställningar", "view": "Visa" }, - "repository-health": { - "details": "Detaljer:", - "no-errors-found": "Inga fel hittades", - "title-repository-is-healthy": "Lagringsplatsen är hälsosam", - "title-repository-is-unhealthy": "Lagringsplatsen är ohälsosam" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "Jämför gren", @@ -11609,7 +11617,7 @@ }, "repository-overview": { "checked": "Kontrollerad:", - "finished": "Avslutad:", + "finished": "", "health": "Hälsa", "healthy": "Hälsosamma", "job-id": "Jobb-ID:", @@ -11618,7 +11626,6 @@ "not-available": "Ej tillgängligt", "pull-status": "Pull-status", "resources": "Resurser", - "started": "Startad:", "status": "Status:", "unhealthy": "Ohälsosamma", "view-folder": "Visa mapp", @@ -12278,9 +12285,6 @@ "tokens": "Polletter", "tooltip-managed-service-account-cannot-modified": "Detta är ett hanterat servicekonto och kan inte ändras" }, - "service-account-permissions": { - "title-permissions": "Behörigheter" - }, "service-account-profile": { "information": "Information", "label-creation-date": "Skapande datum", @@ -12955,6 +12959,10 @@ "name-show-line-numbers": "Visa radnummer", "name-show-mini-map": "Visa minikarta" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "Instrumentpaneler", diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index bc07c4caf4a..b2aa2f13aa2 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "İzin ekle", + "loading": "", "no-permissions": "İzin yok", "permissions-change-warning": "Bu işlem, bu klasör ve tüm alt klasörlerin izinlerini değiştirecektir. Bu işlem toplamda şunları etkileyecektir:", "role": "Rol", "serviceaccount": "Hizmet Hesabı", "team": "Ekip", - "title": "İzinler", "user": "Kullanıcı" } }, @@ -3504,7 +3504,7 @@ "delete-button": "Sil", "delete-modal-invalid-text": "Bir veya daha fazla klasör, kütüphane panelleri veya uyarı kuralları içeriyor. Devam etmek için öncelikle bunları silin.", "delete-modal-invalid-title": "Klasör silinemiyor", - "delete-modal-restore-dashboards-text": "Bu işlem, seçilen klasörleri anında silecektir ancak seçilen panolar 30 gün içinde silinmek üzere işaretlenecektir. Kuruluş yöneticiniz, panoları 30 günlük süre dolmadan önce istediği zaman geri yükleyebilir. Klasörler geri yüklenemez.", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "Bu işlem aşağıdaki içerikleri silecektir:", "delete-modal-title": "Sil", "delete-provisioned-folder": "Sağlanan klasörü sil", @@ -4572,7 +4572,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "Koşullu oluşturma nedeniyle öge gizlendi." + "tooltip": "" }, "root": { "title": "Kuralları göster/gizle" @@ -5819,6 +5819,7 @@ "dynamically-switch-source-multiple-panels": "Birden fazla panel için veri kaynağını dinamik olarak değiştirin", "group": "Anında gruplandırma için anahtarlar ekleyin", "hidden-constant-variable": "Paylaşmak istediğiniz panolardaki metrik önekler için kullanışlı olan gizli bir sabit değişken", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "Kullanıcılar bir metin kutusuna herhangi bir dizi girebilir", "values-are-static-and-defined-manually": "Değerler statiktir ve manuel olarak tanımlanır", "values-fetched-source-query": "Değerler bir veri kaynağı sorgusundan alınır", @@ -5832,6 +5833,7 @@ "group-by": "Gruplandır", "interval": "Aralık", "query": "Sorgu", + "switch": "", "textbox": "Metin kutusu" } }, @@ -6176,6 +6178,17 @@ "copy-to-clipboard-failed": "Panoya kopyalanamadı" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "Varsayılan değer" }, @@ -10355,7 +10368,7 @@ "title": "Profiller" }, "recently-deleted": { - "subtitle": "Burada 30 günden fazla listelenen ögeler otomatik olarak silinecektir.", + "subtitle": "", "title": "Son silinenler" }, "recorded-queries": { @@ -11571,6 +11584,7 @@ "active-jobs": "etkin işler", "column-action": "Eylem", "column-duration": "Süre", + "column-job-id": "", "column-message": "Mesaj", "column-started": "Başlatıldı", "column-status": "Durum", @@ -11589,12 +11603,6 @@ "settings": "Ayarlar", "view": "Görüntüle" }, - "repository-health": { - "details": "Detaylar: ", - "no-errors-found": "Hata bulunmadı", - "title-repository-is-healthy": "Depo iyi durumda", - "title-repository-is-unhealthy": "Depo iyi durumda değil" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "", @@ -11609,7 +11617,7 @@ }, "repository-overview": { "checked": "Kontrol Eden:", - "finished": "Bitiş:", + "finished": "", "health": "Sağlık", "healthy": "İyi durumda", "job-id": "İş Kimliği:", @@ -11618,7 +11626,6 @@ "not-available": "Geçersiz", "pull-status": "Çekme durumu", "resources": "Kaynaklar", - "started": "Başlangıç:", "status": "Durum:", "unhealthy": "İyi durumda değil", "view-folder": "Klasörü Görüntüle", @@ -12278,9 +12285,6 @@ "tokens": "Belirteçler", "tooltip-managed-service-account-cannot-modified": "Bu yönetilen bir hizmet hesabıdır ve değiştirilemez" }, - "service-account-permissions": { - "title-permissions": "İzinler" - }, "service-account-profile": { "information": "Bilgi", "label-creation-date": "Oluşturulma Tarihi", @@ -12955,6 +12959,10 @@ "name-show-line-numbers": "Satır numaralarını göster", "name-show-mini-map": "Mini haritayı göster" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "Panolar", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index c4e73e86396..c68822d2b8d 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "添加权限", + "loading": "", "no-permissions": "没有任何权限", "permissions-change-warning": "这将更改此文件夹及其所有子文件夹的权限。总体而言,这将影响:", "role": "角色", "serviceaccount": "服务帐户", "team": "团队", - "title": "权限", "user": "用户" } }, @@ -3492,7 +3492,7 @@ "delete-button": "删除", "delete-modal-invalid-text": "一个或多个文件夹包含库面板或警报规则。请首先删除这些以便继续。", "delete-modal-invalid-title": "无法删除文件夹", - "delete-modal-restore-dashboards-text": "此操作将立即删除所选文件夹,但所选数据面板将被标记为在 30 天后删除。您的组织管理员可以在 30 天期限内随时还原数据面板。文件夹无法还原。", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "此操作将删除以下内容:", "delete-modal-title": "删除", "delete-provisioned-folder": "删除已预置的文件夹", @@ -4552,7 +4552,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "由于条件渲染,元素已被隐藏。" + "tooltip": "" }, "root": { "title": "显示/隐藏规则" @@ -5798,6 +5798,7 @@ "dynamically-switch-source-multiple-panels": "动态切换多个面板的数据源", "group": "即时添加按键分组", "hidden-constant-variable": "隐藏的常量变量,对于您要共享的数据面板中的指标前缀很有用", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "用户可以在文本框中输入任意字符串", "values-are-static-and-defined-manually": "值为静态,由手动定义", "values-fetched-source-query": "值从数据源查询中获取", @@ -5811,6 +5812,7 @@ "group-by": "分组方式", "interval": "间隔", "query": "查询", + "switch": "", "textbox": "文本框" } }, @@ -6153,6 +6155,17 @@ "copy-to-clipboard-failed": "复制到剪贴板失败" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "默认值" }, @@ -10324,7 +10337,7 @@ "title": "个人资料" }, "recently-deleted": { - "subtitle": "此处列出的任何项目超过 30 天都将被自动删除。", + "subtitle": "", "title": "最近删除" }, "recorded-queries": { @@ -11531,6 +11544,7 @@ "active-jobs": "活跃的作业", "column-action": "操作", "column-duration": "持续时间", + "column-job-id": "", "column-message": "消息", "column-started": "已启动", "column-status": "状态", @@ -11549,12 +11563,6 @@ "settings": "设置", "view": "查看" }, - "repository-health": { - "details": "详情:", - "no-errors-found": "没有发现错误", - "title-repository-is-healthy": "存储库状态良好", - "title-repository-is-unhealthy": "存储库状态不良" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "比较分支", @@ -11569,7 +11577,7 @@ }, "repository-overview": { "checked": "已检查:", - "finished": "已完成:", + "finished": "", "health": "健康", "healthy": "状态良好", "job-id": "作业 ID:", @@ -11578,7 +11586,6 @@ "not-available": "N/A", "pull-status": "拉取状态", "resources": "资源", - "started": "已开始:", "status": "状态:", "unhealthy": "状态不良", "view-folder": "查看文件夹", @@ -12235,9 +12242,6 @@ "tokens": "令牌", "tooltip-managed-service-account-cannot-modified": "这是托管服务账户,无法修改" }, - "service-account-permissions": { - "title-permissions": "权限" - }, "service-account-profile": { "information": "信息", "label-creation-date": "创建日期", @@ -12910,6 +12914,10 @@ "name-show-line-numbers": "显示行号", "name-show-mini-map": "显示小地图" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "仪表板", diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 2e783714b46..91502e2e2d8 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -27,12 +27,12 @@ }, "permissions": { "add-label": "新增權限", + "loading": "", "no-permissions": "沒有權限", "permissions-change-warning": "這將變更此資料夾及其所有子系的權限。這會影響以下項目:", "role": "角色", "serviceaccount": "服務帳戶", "team": "團隊", - "title": "權限", "user": "使用者" } }, @@ -3492,7 +3492,7 @@ "delete-button": "刪除", "delete-modal-invalid-text": "一個或多個資料夾包含資料庫面板或警報規則。請先刪除這些資料,然後再繼續。", "delete-modal-invalid-title": "無法刪除資料夾", - "delete-modal-restore-dashboards-text": "此動作會立即刪除選取的資料夾,但選取的儀表板會標記在 30 天後刪除。您的組織管理員可以在 30 天到期之前隨時還原儀表板。資料夾無法還原。", + "delete-modal-restore-dashboards-text": "", "delete-modal-text": "此動作將刪除以下內容:", "delete-modal-title": "刪除", "delete-provisioned-folder": "刪除已佈建的資料夾", @@ -4552,7 +4552,7 @@ "unsupported-object-type": "" }, "overlay": { - "tooltip": "由於條件轉譯,元素已被隱藏。" + "tooltip": "" }, "root": { "title": "顯示/隱藏規則" @@ -5798,6 +5798,7 @@ "dynamically-switch-source-multiple-panels": "動態切換多個面板的資料來源", "group": "即時新增鍵以便進行分組", "hidden-constant-variable": "隱藏的常數變數,對於您要共用的儀表板中的指標前置詞很有用", + "users-enter-arbitrary-strings-switch": "", "users-enter-arbitrary-strings-textbox": "使用者可以在文字方塊中輸入任何任意字串", "values-are-static-and-defined-manually": "值是靜態的,並且是手動定義的", "values-fetched-source-query": "從資料來源查詢中擷取值", @@ -5811,6 +5812,7 @@ "group-by": "分組依據", "interval": "間隔", "query": "查詢", + "switch": "", "textbox": "文字方塊" } }, @@ -6153,6 +6155,17 @@ "copy-to-clipboard-failed": "複製到剪貼簿失敗" } }, + "switch-variable-form": { + "disabled-value": "", + "disabled-value-description": "", + "disabled-value-placeholder": "", + "enabled-value": "", + "enabled-value-description": "", + "enabled-value-placeholder": "", + "switch-options": "", + "value-pair-type": "", + "value-pair-type-description": "" + }, "text-box-variable": { "name-default-value": "預設值" }, @@ -10324,7 +10337,7 @@ "title": "個人資料" }, "recently-deleted": { - "subtitle": "此處列出超過 30 天的任何項目將被自動刪除。", + "subtitle": "", "title": "最近刪除" }, "recorded-queries": { @@ -11531,6 +11544,7 @@ "active-jobs": "進行中的作業", "column-action": "動作", "column-duration": "持續時間", + "column-job-id": "", "column-message": "訊息", "column-started": "已開始", "column-status": "狀態", @@ -11549,12 +11563,6 @@ "settings": "設定", "view": "檢視" }, - "repository-health": { - "details": "詳細資料:", - "no-errors-found": "沒有發現錯誤", - "title-repository-is-healthy": "儲存庫狀態良好", - "title-repository-is-unhealthy": "儲存庫狀態不佳" - }, "repository-link": { "delete-or-move-job": { "compare-branch": "比較分支", @@ -11569,7 +11577,7 @@ }, "repository-overview": { "checked": "已選取:", - "finished": "結束:", + "finished": "", "health": "使用情況", "healthy": "健全", "job-id": "作業 ID:", @@ -11578,7 +11586,6 @@ "not-available": "不適用", "pull-status": "拉取狀態", "resources": "資源", - "started": "開始:", "status": "狀態:", "unhealthy": "狀況不佳", "view-folder": "檢視資料夾", @@ -12235,9 +12242,6 @@ "tokens": "權杖", "tooltip-managed-service-account-cannot-modified": "這是受管理的服務帳戶,無法修改" }, - "service-account-permissions": { - "title-permissions": "權限" - }, "service-account-profile": { "information": "資訊", "label-creation-date": "建立日期", @@ -12910,6 +12914,10 @@ "name-show-line-numbers": "顯示行號", "name-show-mini-map": "顯示迷你地圖" }, + "theme-playground": { + "label-base-theme": "", + "title": "" + }, "theme-preview": { "breadcrumbs": { "dashboards": "儀表板", From 09a3498552bbf8b2997c737f19fbc1082cd16228 Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Fri, 10 Oct 2025 13:23:23 +0100 Subject: [PATCH 124/578] Open Grafana Pathfinder instead of the help menu if its installed (#110592) * Link to Grafana Pathfinder if available Signed-off-by: Jack Baldry * refactor: `getComponentIdFromComponentMeta()` only receives the title * Making sure we pass helpNode without Parents to the pathfinder app. * minor refactoring to isolate the code. * Fix tests Signed-off-by: Jack Baldry * cleaned up the structure and exposing the helpNavItem via a hook * added missing files. * Add support for old and new pathfinder IDs Signed-off-by: Jack Baldry * Rename hook for consistency Signed-off-by: Jack Baldry --------- Signed-off-by: Jack Baldry Co-authored-by: Levente Balogh Co-authored-by: Marcus Andersson --- .../grafana-runtime/src/services/index.ts | 1 + .../src/services/navigation/useHelpNavItem.ts | 20 +++++ pkg/services/navtree/navtreeimpl/navtree.go | 16 +++- public/app/app.ts | 3 + .../ExtensionSidebar.test.tsx | 2 +- .../ExtensionSidebarProvider.test.tsx | 10 +-- .../ExtensionSidebarProvider.tsx | 21 +++++- .../ExtensionSidebar/ExtensionToolbarItem.tsx | 18 ++++- .../AppChrome/TopBar/HelpTopBarButton.tsx | 75 +++++++++++++++++++ .../AppChrome/TopBar/SingleTopBar.tsx | 19 ++--- .../AppChrome/TopBar/useHelpNode.tsx | 11 +++ 11 files changed, 168 insertions(+), 28 deletions(-) create mode 100644 packages/grafana-runtime/src/services/navigation/useHelpNavItem.ts create mode 100644 public/app/core/components/AppChrome/TopBar/HelpTopBarButton.tsx create mode 100644 public/app/core/components/AppChrome/TopBar/useHelpNode.tsx diff --git a/packages/grafana-runtime/src/services/index.ts b/packages/grafana-runtime/src/services/index.ts index e9d2eeac852..42ef1c3f655 100644 --- a/packages/grafana-runtime/src/services/index.ts +++ b/packages/grafana-runtime/src/services/index.ts @@ -30,6 +30,7 @@ export { type UsePluginFunctionsOptions, type UsePluginFunctionsResult, } from './pluginExtensions/usePluginFunctions'; +export { setHelpNavItemHook, useHelpNavItem, type UseHelpNavItem } from './navigation/useHelpNavItem'; export { getObservablePluginLinks } from './pluginExtensions/getObservablePluginLinks'; export { getObservablePluginComponents } from './pluginExtensions/getObservablePluginComponents'; export { diff --git a/packages/grafana-runtime/src/services/navigation/useHelpNavItem.ts b/packages/grafana-runtime/src/services/navigation/useHelpNavItem.ts new file mode 100644 index 00000000000..cb39aee2c0e --- /dev/null +++ b/packages/grafana-runtime/src/services/navigation/useHelpNavItem.ts @@ -0,0 +1,20 @@ +import { NavModelItem } from '@grafana/data'; + +export type UseHelpNavItem = () => NavModelItem | undefined; + +let singleton: UseHelpNavItem | undefined; + +export function setHelpNavItemHook(hook: UseHelpNavItem): void { + // We allow overriding the registry in tests + if (singleton && process.env.NODE_ENV !== 'test') { + throw new Error('setHelpNavItemHook() function should only be called once, when Grafana is starting.'); + } + singleton = hook; +} + +export function useHelpNavItem(): NavModelItem | undefined { + if (!singleton) { + throw new Error('useHelpNavItem() can only be used after the Grafana instance has started.'); + } + return singleton(); +} diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 000ead39266..08853afa2bf 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -59,7 +59,8 @@ type NavigationAppConfig struct { func ProvideService(cfg *setting.Cfg, accessControl ac.AccessControl, pluginStore pluginstore.Store, pluginSettings pluginsettings.Service, starService star.Service, features featuremgmt.FeatureToggles, dashboardService dashboards.DashboardService, accesscontrolService ac.Service, kvStore kvstore.KVStore, apiKeyService apikey.Service, - license licensing.Licensing, authnService authn.Service) navtree.Service { + license licensing.Licensing, authnService authn.Service, +) navtree.Service { service := &ServiceImpl{ cfg: cfg, log: log.New("navtree service"), @@ -243,9 +244,10 @@ func isSupportBundlesEnabled(s *ServiceImpl) bool { return s.cfg.SectionWithEnvOverrides("support_bundles").Key("enabled").MustBool(true) } +// addHelpLinks adds a help menu item to the navigation bar. +// If the Grafana Pathfinder plugin is installed, it will handle enriching the help menu. func (s *ServiceImpl) addHelpLinks(treeRoot *navtree.NavTreeRoot, c *contextmodel.ReqContext) { if s.cfg.HelpEnabled { - // The version subtitle is set later by NavTree.ApplyHelpVersion helpNode := &navtree.NavLink{ Text: "Help", Id: "help", @@ -257,6 +259,16 @@ func (s *ServiceImpl) addHelpLinks(treeRoot *navtree.NavTreeRoot, c *contextmode treeRoot.AddSection(helpNode) + ctx := c.Req.Context() + // The docs plugin ID is going to transition from grafana-grafanadocsplugin-app to grafana-pathfinder-app. + // Support both until that migration is complete. + _, oldPathfinderInstalled := s.pluginStore.Plugin(ctx, "grafana-grafanadocsplugin-app") + _, newPathfinderInstalled := s.pluginStore.Plugin(ctx, "grafana-pathfinder-app") + if oldPathfinderInstalled || newPathfinderInstalled { + // Add a custom property to indicate this should open Grafana Pathfinder. + helpNode.HideFromTabs = true + } + hasAccess := ac.HasAccess(s.accessControl, c) supportBundleAccess := ac.EvalAny( ac.EvalPermission(supportbundlesimpl.ActionRead), diff --git a/public/app/app.ts b/public/app/app.ts index 7896117c1e7..48b97f40024 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -37,6 +37,7 @@ import { setCurrentUser, setChromeHeaderHeightHook, setPluginLinksHook, + setHelpNavItemHook, setFolderPicker, setCorrelationsService, setPluginFunctionsHook, @@ -60,6 +61,7 @@ import { AppWrapper } from './AppWrapper'; import appEvents from './core/app_events'; import { AppChromeService } from './core/components/AppChrome/AppChromeService'; import { useChromeHeaderHeight } from './core/components/AppChrome/TopBar/useChromeHeaderHeight'; +import { useHelpNode } from './core/components/AppChrome/TopBar/useHelpNode'; import { LazyFolderPicker } from './core/components/NestedFolderPicker/LazyFolderPicker'; import { getAllOptionEditors, getAllStandardFieldConfigs } from './core/components/OptionsUI/registry'; import { PluginPage } from './core/components/Page/PluginPage'; @@ -260,6 +262,7 @@ export class GrafanaApp { await preloadPlugins(appPluginsToAwait); } + setHelpNavItemHook(useHelpNode); setPluginLinksHook(usePluginLinks); setPluginComponentHook(usePluginComponent); setPluginComponentsHook(usePluginComponents); diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.test.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.test.tsx index 3891dac8e50..e08a37fda26 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.test.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.test.tsx @@ -43,7 +43,7 @@ const addedComponentConfigMock: ExtensionInfo = { }; const extensionSidebarContextMock: ExtensionSidebarContextType = { - dockedComponentId: getComponentIdFromComponentMeta(pluginId, addedComponentConfigMock), + dockedComponentId: getComponentIdFromComponentMeta(pluginId, addedComponentConfigMock.title), props: {}, isOpen: true, setDockedComponentId: jest.fn(), diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx index 7839207632b..955c85923b6 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx @@ -121,7 +121,7 @@ describe('ExtensionSidebarProvider', () => { }); it('should load docked component from storage if available', () => { - const componentId = getComponentIdFromComponentMeta(mockPluginMeta.pluginId, mockComponent); + const componentId = getComponentIdFromComponentMeta(mockPluginMeta.pluginId, mockComponent.title); (store.get as jest.Mock).mockReturnValue(componentId); render( @@ -135,7 +135,7 @@ describe('ExtensionSidebarProvider', () => { }); it('should update storage when docked component changes', () => { - const componentId = getComponentIdFromComponentMeta(mockPluginMeta.pluginId, mockComponent); + const componentId = getComponentIdFromComponentMeta(mockPluginMeta.pluginId, mockComponent.title); const TestComponentWithActions = () => { const context = useExtensionSidebarContext(); @@ -291,7 +291,7 @@ describe('ExtensionSidebarProvider', () => { }); it('should close sidebar when receiving a CloseExtensionSidebarEvent', () => { - const componentId = getComponentIdFromComponentMeta(mockPluginMeta.pluginId, mockComponent); + const componentId = getComponentIdFromComponentMeta(mockPluginMeta.pluginId, mockComponent.title); const TestComponentWithProps = () => { const context = useExtensionSidebarContext(); @@ -433,7 +433,7 @@ describe('ExtensionSidebarProvider', () => { describe('Utility Functions', () => { describe('getComponentIdFromComponentMeta', () => { it('should create a valid component ID', () => { - const componentId = getComponentIdFromComponentMeta(mockPluginMeta.pluginId, mockComponent); + const componentId = getComponentIdFromComponentMeta(mockPluginMeta.pluginId, mockComponent.title); expect(componentId).toBe( JSON.stringify({ pluginId: mockPluginMeta.pluginId, componentTitle: mockComponent.title }) @@ -443,7 +443,7 @@ describe('Utility Functions', () => { describe('getComponentMetaFromComponentId', () => { it('should parse a valid component ID', () => { - const componentId = getComponentIdFromComponentMeta(mockPluginMeta.pluginId, mockComponent); + const componentId = getComponentIdFromComponentMeta(mockPluginMeta.pluginId, mockComponent.title); const meta = getComponentMetaFromComponentId(componentId); expect(meta).toEqual({ diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx index ca5ff926169..9dcb1cf1edb 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx @@ -1,7 +1,7 @@ import { createContext, ReactNode, useCallback, useContext, useEffect, useState, useMemo } from 'react'; import { useLocalStorage } from 'react-use'; -import { PluginExtensionPoints, store, type ExtensionInfo } from '@grafana/data'; +import { PluginExtensionPoints, store } from '@grafana/data'; import { getAppEvents, reportInteraction, usePluginLinks, locationService } from '@grafana/runtime'; import { ExtensionPointPluginMeta, getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils'; import { CloseExtensionSidebarEvent, OpenExtensionSidebarEvent } from 'app/types/events'; @@ -228,8 +228,8 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo ); }; -export function getComponentIdFromComponentMeta(pluginId: string, component: ExtensionInfo) { - return JSON.stringify({ pluginId, componentTitle: component.title }); +export function getComponentIdFromComponentMeta(pluginId: string, componentTitle: string) { + return JSON.stringify({ pluginId, componentTitle }); } export function getComponentMetaFromComponentId( @@ -252,3 +252,18 @@ export function getComponentMetaFromComponentId( return undefined; } } + +// The docs plugin ID is going to transition from grafana-grafanadocsplugin-app to grafana-pathfinder-app. +// Support both until that migration is complete. +// Prioritize the new plugin ID (grafana-pathfinder-app). +export function getPathfinderPluginId(availableComponents: ExtensionPointPluginMeta): string | undefined { + if (availableComponents.has('grafana-pathfinder-app')) { + return 'grafana-pathfinder-app'; + } + + if (availableComponents.has('grafana-grafanadocsplugin-app')) { + return 'grafana-grafanadocsplugin-app'; + } + + return undefined; +} diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx index 3839ebcfcaa..cee522a2e85 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx @@ -17,20 +17,32 @@ type Props = { }; const compactAllowedComponents = ['grafana-assistant-app']; +const pathfinderPluginIds = ['grafana-pathfinder-app', 'grafana-grafanadocsplugin-app']; export function ExtensionToolbarItem({ compact }: Props) { const { availableComponents, dockedComponentId, setDockedComponentId } = useExtensionSidebarContext(); - if (availableComponents.size === 0) { + // Don't render the toolbar if the only available plugins are Grafana Pathfinder. + // It's opened by the help menu. + const nonPathfinderPlugins = Array.from(availableComponents.keys()).filter( + (pluginId) => !pathfinderPluginIds.includes(pluginId) + ); + if (nonPathfinderPlugins.length === 0) { return null; } const dockedMeta = dockedComponentId ? getComponentMetaFromComponentId(dockedComponentId) : null; const renderPluginButton = (pluginId: string, components: ComponentWithPluginId[]) => { + // Don't render the Grafana Pathfinder button. + // It's opened by the help menu button. + if (pathfinderPluginIds.includes(pluginId)) { + return null; + } + if (components.length === 1) { const component = components[0]; - const componentId = getComponentIdFromComponentMeta(pluginId, component); + const componentId = getComponentIdFromComponentMeta(pluginId, component.title); const isActive = dockedComponentId === componentId; // we now allow more components in the extension sidebar @@ -54,7 +66,7 @@ export function ExtensionToolbarItem({ compact }: Props) { const MenuItems = ( {components.map((c) => { - const id = getComponentIdFromComponentMeta(pluginId, c); + const id = getComponentIdFromComponentMeta(pluginId, c.title); return ( } placement="bottom-end"> + + + ); + } + + const componentId = getComponentIdFromComponentMeta(pathfinderPluginId, 'Grafana Pathfinder'); + const isOpen = dockedComponentId === componentId; + + return ( + { + if (isOpen) { + setDockedComponentId(undefined); + } else { + const appEvents = getAppEvents(); + appEvents.publish( + new OpenExtensionSidebarEvent({ + pluginId: pathfinderPluginId, + componentTitle: 'Grafana Pathfinder', + }) + ); + } + }} + /> + ); +}); + +const getStyles = (theme: GrafanaTheme2) => ({ + helpButtonActive: css({ + borderRadius: theme.shape.radius.circle, + backgroundColor: theme.colors.primary.transparent, + border: `1px solid ${theme.colors.primary.borderTransparent}`, + color: theme.colors.text.primary, + }), +}); diff --git a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx index 71211e4b535..f04222533f9 100644 --- a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx +++ b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx @@ -1,12 +1,11 @@ import { css } from '@emotion/css'; -import { cloneDeep } from 'lodash'; -import { memo } from 'react'; +import React, { memo } from 'react'; import { GrafanaTheme2, NavModelItem } from '@grafana/data'; import { Components } from '@grafana/e2e-selectors'; import { t } from '@grafana/i18n'; import { ScopesContextValue } from '@grafana/runtime'; -import { Dropdown, Icon, Stack, ToolbarButton, useStyles2 } from '@grafana/ui'; +import { Icon, Stack, ToolbarButton, useStyles2 } from '@grafana/ui'; import { config } from 'app/core/config'; import { MEGA_MENU_TOGGLE_ID } from 'app/core/constants'; import { useGrafana } from 'app/core/context/GrafanaContext'; @@ -20,15 +19,14 @@ import { Breadcrumbs } from '../../Breadcrumbs/Breadcrumbs'; import { buildBreadcrumbs } from '../../Breadcrumbs/utils'; import { ExtensionToolbarItem } from '../ExtensionSidebar/ExtensionToolbarItem'; import { HistoryContainer } from '../History/HistoryContainer'; -import { enrichHelpItem } from '../MegaMenu/utils'; import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator'; import { QuickAdd } from '../QuickAdd/QuickAdd'; +import { HelpTopBarButton } from './HelpTopBarButton'; import { InviteUserButton } from './InviteUserButton'; import { ProfileButton } from './ProfileButton'; import { SignInLink } from './SignInLink'; import { SingleTopBarActions } from './SingleTopBarActions'; -import { TopNavBarMenu } from './TopNavBarMenu'; import { TopSearchBarCommandPaletteTrigger } from './TopSearchBarCommandPaletteTrigger'; import { getChromeHeaderLevelHeight } from './useChromeHeaderHeight'; @@ -57,10 +55,7 @@ export const SingleTopBar = memo(function SingleTopBar({ const state = chrome.useState(); const menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen; const styles = useStyles2(getStyles, menuDockedAndOpen); - const navIndex = useSelector((state) => state.navIndex); - const helpNode = cloneDeep(navIndex['help']); - const enrichedHelpNode = helpNode ? enrichHelpItem(helpNode) : undefined; - const profileNode = navIndex['profile']; + const profileNode = useSelector((state) => state.navIndex['profile']); const homeNav = useSelector((state) => state.navIndex)[HOME_NAV_ID]; const breadcrumbs = buildBreadcrumbs(sectionNav, pageNav, homeNav); const unifiedHistoryEnabled = config.featureToggles.unifiedHistory; @@ -98,11 +93,7 @@ export const SingleTopBar = memo(function SingleTopBar({ {unifiedHistoryEnabled && !isSmallScreen && } {!isSmallScreen && } - {enrichedHelpNode && ( - } placement="bottom-end"> - - - )} + {!isSmallScreen && } {!showToolbarLevel && actions} diff --git a/public/app/core/components/AppChrome/TopBar/useHelpNode.tsx b/public/app/core/components/AppChrome/TopBar/useHelpNode.tsx new file mode 100644 index 00000000000..52beedffb56 --- /dev/null +++ b/public/app/core/components/AppChrome/TopBar/useHelpNode.tsx @@ -0,0 +1,11 @@ +import { cloneDeep } from 'lodash'; + +import { useSelector } from 'app/types/store'; + +import { enrichHelpItem } from '../MegaMenu/utils'; + +export function useHelpNode() { + const navIndex = useSelector((state) => state.navIndex); + const helpNode = cloneDeep(navIndex['help']); + return helpNode ? enrichHelpItem(helpNode) : undefined; +} From fdccc3e33c535e5fff5c9960c8ed8a23e1c335b2 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 10 Oct 2025 15:37:32 +0300 Subject: [PATCH 125/578] Dashboards/API: Keep original when conversion fails (#109912) * keep original * openapi * update tests * workspace * workspace * fix ts * merge main * update snapshots --- apps/dashboard/kinds/dashboard.cue | 3 + .../v0alpha1/dashboard_status_gen.go | 2 + .../v0alpha1/zz_generated.openapi.go | 7 + .../dashboard/v1beta1/dashboard_status_gen.go | 2 + .../dashboard/v1beta1/zz_generated.openapi.go | 7 + .../v2alpha1/dashboard_status_gen.go | 2 + .../v2alpha1/zz_generated.openapi.go | 7 + .../dashboard/v2beta1/dashboard_status_gen.go | 2 + .../dashboard/v2beta1/zz_generated.openapi.go | 7 + .../output/v2alpha1.complete.v0alpha1.json | 500 +++++++- .../output/v2alpha1.complete.v1beta1.json | 500 +++++++- .../v2alpha1.ds-data-query.v0alpha1.json | 1050 ++++++++++++++++- .../v2alpha1.ds-data-query.v1beta1.json | 1050 ++++++++++++++++- .../v2alpha1.groupby-adhoc-vars.v0alpha1.json | 95 +- .../v2alpha1.groupby-adhoc-vars.v1beta1.json | 95 +- .../output/v2alpha1.viz-config.v0alpha1.json | 214 +++- .../output/v2alpha1.viz-config.v1beta1.json | 214 +++- apps/dashboard/pkg/migration/conversion/v0.go | 2 + apps/dashboard/pkg/migration/conversion/v1.go | 2 + apps/dashboard/pkg/migration/conversion/v2.go | 6 + .../dashboard/v0alpha1/types.status.gen.ts | 2 + .../dashboard/v1beta1/types.status.gen.ts | 2 + .../dashboard/v2alpha1/types.status.gen.ts | 2 + .../dashboard/v2beta1/types.status.gen.ts | 2 + .../dashboard.grafana.app-v0alpha1.json | 4 + .../dashboard.grafana.app-v1beta1.json | 4 + .../dashboard.grafana.app-v2alpha1.json | 4 + 27 files changed, 3779 insertions(+), 8 deletions(-) diff --git a/apps/dashboard/kinds/dashboard.cue b/apps/dashboard/kinds/dashboard.cue index 905b9606336..e8dfea3bf98 100644 --- a/apps/dashboard/kinds/dashboard.cue +++ b/apps/dashboard/kinds/dashboard.cue @@ -27,6 +27,9 @@ ConversionStatus: { // The version which was stored when the dashboard was created / updated. // Fetching this version should always succeed. storedVersion?: string + + // The original value map[string]any + source?: _ } dashboard: { diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_status_gen.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_status_gen.go index 2dec6aea951..1db6c8e3467 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_status_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_status_gen.go @@ -15,6 +15,8 @@ type DashboardConversionStatus struct { // The version which was stored when the dashboard was created / updated. // Fetching this version should always succeed. StoredVersion *string `json:"storedVersion,omitempty"` + // The original value map[string]any + Source interface{} `json:"source,omitempty"` } // NewDashboardConversionStatus creates a new DashboardConversionStatus object. diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go index 0027a8b8cc4..63af5e26b7e 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go @@ -272,6 +272,13 @@ func schema_pkg_apis_dashboard_v0alpha1_DashboardConversionStatus(ref common.Ref Format: "", }, }, + "source": { + SchemaProps: spec.SchemaProps{ + Description: "The original value map[string]any", + Type: []string{"object"}, + Format: "", + }, + }, }, Required: []string{"failed"}, }, diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_status_gen.go b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_status_gen.go index a2ac9850b09..5fa18c8f821 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_status_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_status_gen.go @@ -15,6 +15,8 @@ type DashboardConversionStatus struct { // The version which was stored when the dashboard was created / updated. // Fetching this version should always succeed. StoredVersion *string `json:"storedVersion,omitempty"` + // The original value map[string]any + Source interface{} `json:"source,omitempty"` } // NewDashboardConversionStatus creates a new DashboardConversionStatus object. diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v1beta1/zz_generated.openapi.go index ab68539f1fe..be747be2ae3 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/zz_generated.openapi.go @@ -264,6 +264,13 @@ func schema_pkg_apis_dashboard_v1beta1_DashboardConversionStatus(ref common.Refe Format: "", }, }, + "source": { + SchemaProps: spec.SchemaProps{ + Description: "The original value map[string]any", + Type: []string{"object"}, + Format: "", + }, + }, }, Required: []string{"failed"}, }, diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_status_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_status_gen.go index 9544b902dd8..6fad54db0b4 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_status_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_status_gen.go @@ -15,6 +15,8 @@ type DashboardConversionStatus struct { // The version which was stored when the dashboard was created / updated. // Fetching this version should always succeed. StoredVersion *string `json:"storedVersion,omitempty"` + // The original value map[string]any + Source interface{} `json:"source,omitempty"` } // NewDashboardConversionStatus creates a new DashboardConversionStatus object. diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go index 1c030f705c9..548a8c93268 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go @@ -1353,6 +1353,13 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardConversionStatus(ref common.Ref Format: "", }, }, + "source": { + SchemaProps: spec.SchemaProps{ + Description: "The original value map[string]any", + Type: []string{"object"}, + Format: "", + }, + }, }, Required: []string{"failed"}, }, diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_status_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_status_gen.go index 62d39eb766d..a6bd3ee5e18 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_status_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_status_gen.go @@ -15,6 +15,8 @@ type DashboardConversionStatus struct { // The version which was stored when the dashboard was created / updated. // Fetching this version should always succeed. StoredVersion *string `json:"storedVersion,omitempty"` + // The original value map[string]any + Source interface{} `json:"source,omitempty"` } // NewDashboardConversionStatus creates a new DashboardConversionStatus object. diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go index 39e6e37503f..5e51134e80c 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go @@ -1360,6 +1360,13 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardConversionStatus(ref common.Refe Format: "", }, }, + "source": { + SchemaProps: spec.SchemaProps{ + Description: "The original value map[string]any", + Type: []string{"object"}, + Format: "", + }, + }, }, Required: []string{"failed"}, }, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v0alpha1.json index 1191a6aef3b..3306c334c5e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v0alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v0alpha1.json @@ -15,7 +15,505 @@ "conversion": { "failed": true, "error": "backend conversion not yet implemented", - "storedVersion": "v2alpha1" + "storedVersion": "v2alpha1", + "source": { + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "test-v2alpha1-complete", + "labels": { + "category": "test" + }, + "annotations": { + "description": "Complete example of v2alpha1 dashboard features" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "grafana", + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "expr": "changes(process_start_time_seconds[1m])", + "refId": "Anno" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "Prometheus Annotations", + "builtIn": false + } + } + ], + "cursorSync": "Tooltip", + "description": "This dashboard demonstrates all features that need to be converted from v2alpha1 to v2beta1", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with Conditional Rendering", + "description": "This panel demonstrates conditional rendering features", + "links": null, + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "up{job=\"grafana\"}" + } + }, + "datasource": { + "uid": "gdev-prometheus" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "includeTimeField": false, + "mode": "reduceFields", + "reducers": [ + "mean" + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "Down", + "color": "red" + }, + "1": { + "text": "Up", + "color": "green" + } + } + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "red" + }, + { + "value": 1, + "color": "green" + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "Row", + "spec": { + "title": "Conditional Row", + "collapse": false, + "hideHeader": false, + "fillScreen": false, + "conditionalRendering": { + "kind": "ConditionalRenderingGroup", + "spec": { + "visibility": "show", + "condition": "and", + "items": [ + { + "kind": "ConditionalRenderingVariable", + "spec": { + "variable": "group_by", + "operator": "includes", + "value": "instance" + } + }, + { + "kind": "ConditionalRenderingData", + "spec": { + "value": true + } + }, + { + "kind": "ConditionalRenderingTimeRangeSize", + "spec": { + "value": "1h" + } + } + ] + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": null, + "liveNow": true, + "preload": true, + "tags": [ + "test", + "example", + "migration" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "10s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "weekStart": "monday", + "fiscalYearStartMonth": 0 + }, + "title": "Test: Complete V2alpha1 Dashboard Example", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "prometheus_query", + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "label": "Prometheus Query", + "hide": "dontHide", + "refresh": "time", + "skipUrlSync": false, + "description": "Shows all up metrics", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "alphabetical", + "definition": "up", + "options": null, + "multi": true, + "includeAll": true, + "allowCustomValue": false + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "text_var", + "current": { + "selected": true, + "text": "server1", + "value": "server1" + }, + "query": "server1,server2,server3", + "label": "Text Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "A simple text variable" + } + }, + { + "kind": "ConstantVariable", + "spec": { + "name": "constant_var", + "query": "production", + "current": { + "selected": true, + "text": "production", + "value": "production" + }, + "label": "Constant", + "hide": "dontHide", + "skipUrlSync": true, + "description": "A constant value" + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "ds_var", + "pluginId": "prometheus", + "refresh": "load", + "regex": "/^gdev-/", + "current": { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + }, + "options": [ + { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + } + ], + "multi": false, + "includeAll": false, + "label": "Datasource", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Select a datasource", + "allowCustomValue": false + } + }, + { + "kind": "IntervalVariable", + "spec": { + "name": "interval", + "query": "1m,5m,10m,30m,1h,6h,12h,1d", + "current": { + "selected": true, + "text": "5m", + "value": "5m" + }, + "options": [ + { + "text": "1m", + "value": "1m" + }, + { + "text": "5m", + "value": "5m" + }, + { + "text": "10m", + "value": "10m" + }, + { + "text": "30m", + "value": "30m" + }, + { + "text": "1h", + "value": "1h" + }, + { + "text": "6h", + "value": "6h" + }, + { + "text": "12h", + "value": "12h" + }, + { + "text": "1d", + "value": "1d" + } + ], + "auto": true, + "auto_min": "10s", + "auto_count": 30, + "refresh": "load", + "label": "Interval", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Time interval selection" + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_var", + "query": "prod : Production, staging : Staging, dev : Development", + "current": { + "text": [ + "Production" + ], + "value": [ + "prod" + ] + }, + "options": [ + { + "text": "Production", + "value": "prod" + }, + { + "text": "Staging", + "value": "staging" + }, + { + "text": "Development", + "value": "dev" + } + ], + "multi": true, + "includeAll": true, + "allValue": "*", + "label": "Custom Options", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Custom multi-value variable", + "allowCustomValue": true + } + }, + { + "kind": "GroupByVariable", + "spec": { + "name": "group_by", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "current": { + "text": "instance", + "value": "instance" + }, + "options": null, + "multi": false, + "label": "Group By", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Group metrics by label" + } + }, + { + "kind": "AdhocVariable", + "spec": { + "name": "filters", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "baseFilters": [ + { + "key": "job", + "operator": "=", + "value": "grafana", + "condition": "AND" + } + ], + "filters": [], + "defaultKeys": [ + { + "text": "job", + "value": "job", + "expandable": true + }, + { + "text": "instance", + "value": "instance", + "expandable": true + } + ], + "label": "Filters", + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": false + } + } + ] + }, + "status": {} + } } } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v1beta1.json index ed784b45834..c55f325789d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v1beta1.json @@ -15,7 +15,505 @@ "conversion": { "failed": true, "error": "backend conversion not yet implemented", - "storedVersion": "v2alpha1" + "storedVersion": "v2alpha1", + "source": { + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "test-v2alpha1-complete", + "labels": { + "category": "test" + }, + "annotations": { + "description": "Complete example of v2alpha1 dashboard features" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "grafana", + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "expr": "changes(process_start_time_seconds[1m])", + "refId": "Anno" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "Prometheus Annotations", + "builtIn": false + } + } + ], + "cursorSync": "Tooltip", + "description": "This dashboard demonstrates all features that need to be converted from v2alpha1 to v2beta1", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with Conditional Rendering", + "description": "This panel demonstrates conditional rendering features", + "links": null, + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "up{job=\"grafana\"}" + } + }, + "datasource": { + "uid": "gdev-prometheus" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "includeTimeField": false, + "mode": "reduceFields", + "reducers": [ + "mean" + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "Down", + "color": "red" + }, + "1": { + "text": "Up", + "color": "green" + } + } + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "red" + }, + { + "value": 1, + "color": "green" + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "Row", + "spec": { + "title": "Conditional Row", + "collapse": false, + "hideHeader": false, + "fillScreen": false, + "conditionalRendering": { + "kind": "ConditionalRenderingGroup", + "spec": { + "visibility": "show", + "condition": "and", + "items": [ + { + "kind": "ConditionalRenderingVariable", + "spec": { + "variable": "group_by", + "operator": "includes", + "value": "instance" + } + }, + { + "kind": "ConditionalRenderingData", + "spec": { + "value": true + } + }, + { + "kind": "ConditionalRenderingTimeRangeSize", + "spec": { + "value": "1h" + } + } + ] + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": null, + "liveNow": true, + "preload": true, + "tags": [ + "test", + "example", + "migration" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "10s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "weekStart": "monday", + "fiscalYearStartMonth": 0 + }, + "title": "Test: Complete V2alpha1 Dashboard Example", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "prometheus_query", + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "label": "Prometheus Query", + "hide": "dontHide", + "refresh": "time", + "skipUrlSync": false, + "description": "Shows all up metrics", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "alphabetical", + "definition": "up", + "options": null, + "multi": true, + "includeAll": true, + "allowCustomValue": false + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "text_var", + "current": { + "selected": true, + "text": "server1", + "value": "server1" + }, + "query": "server1,server2,server3", + "label": "Text Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "A simple text variable" + } + }, + { + "kind": "ConstantVariable", + "spec": { + "name": "constant_var", + "query": "production", + "current": { + "selected": true, + "text": "production", + "value": "production" + }, + "label": "Constant", + "hide": "dontHide", + "skipUrlSync": true, + "description": "A constant value" + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "ds_var", + "pluginId": "prometheus", + "refresh": "load", + "regex": "/^gdev-/", + "current": { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + }, + "options": [ + { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + } + ], + "multi": false, + "includeAll": false, + "label": "Datasource", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Select a datasource", + "allowCustomValue": false + } + }, + { + "kind": "IntervalVariable", + "spec": { + "name": "interval", + "query": "1m,5m,10m,30m,1h,6h,12h,1d", + "current": { + "selected": true, + "text": "5m", + "value": "5m" + }, + "options": [ + { + "text": "1m", + "value": "1m" + }, + { + "text": "5m", + "value": "5m" + }, + { + "text": "10m", + "value": "10m" + }, + { + "text": "30m", + "value": "30m" + }, + { + "text": "1h", + "value": "1h" + }, + { + "text": "6h", + "value": "6h" + }, + { + "text": "12h", + "value": "12h" + }, + { + "text": "1d", + "value": "1d" + } + ], + "auto": true, + "auto_min": "10s", + "auto_count": 30, + "refresh": "load", + "label": "Interval", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Time interval selection" + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_var", + "query": "prod : Production, staging : Staging, dev : Development", + "current": { + "text": [ + "Production" + ], + "value": [ + "prod" + ] + }, + "options": [ + { + "text": "Production", + "value": "prod" + }, + { + "text": "Staging", + "value": "staging" + }, + { + "text": "Development", + "value": "dev" + } + ], + "multi": true, + "includeAll": true, + "allValue": "*", + "label": "Custom Options", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Custom multi-value variable", + "allowCustomValue": true + } + }, + { + "kind": "GroupByVariable", + "spec": { + "name": "group_by", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "current": { + "text": "instance", + "value": "instance" + }, + "options": null, + "multi": false, + "label": "Group By", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Group metrics by label" + } + }, + { + "kind": "AdhocVariable", + "spec": { + "name": "filters", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "baseFilters": [ + { + "key": "job", + "operator": "=", + "value": "grafana", + "condition": "AND" + } + ], + "filters": [], + "defaultKeys": [ + { + "text": "job", + "value": "job", + "expandable": true + }, + { + "text": "instance", + "value": "instance", + "expandable": true + } + ], + "label": "Filters", + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": false + } + } + ] + }, + "status": {} + } } } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v0alpha1.json index 46365be1c23..4497423cc53 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v0alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v0alpha1.json @@ -9,7 +9,1055 @@ "conversion": { "failed": true, "error": "backend conversion not yet implemented", - "storedVersion": "v2alpha1" + "storedVersion": "v2alpha1", + "source": { + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "test-v2alpha1-annotations" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "grafana", + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "testdata-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "no-ds-testdata-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "prom-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "no-ds-prom-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "PBBCEC2D313BC06C3" + }, + "query": { + "kind": "grafana-postgresql-datasource", + "spec": { + "editorMode": "builder", + "format": "table", + "lines": 10, + "rawSql": "", + "refId": "Anno", + "scenarioId": "annotations", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "postgress-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "elasticsearch", + "uid": "gdev-elasticsearch" + }, + "query": { + "kind": "elasticsearch", + "spec": { + "lines": 10, + "query": "test query", + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "elastic - annos", + "builtIn": false, + "legacyOptions": { + "tagsField": "asd", + "textField": "asd", + "timeEndField": "asdas", + "timeField": "asd" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Simple timeseries (WITH DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "datasource": { + "uid": "gdev-testdata" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Simple stat (NO DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 4 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with NO REF to gdev-prometheus", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with ref to gdev-prometheus", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "datasource": { + "uid": "gdev-prometheus" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Mixed DS WITH REFS", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "datasource": { + "uid": "gdev-prometheus" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "datasource": { + "uid": "gdev-testdata" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Mixed DS WITHOUT REFS", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "maxColumnCount": 3, + "columnWidthMode": "standard", + "rowHeightMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with annotations", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "variable-ds-prometheus", + "current": { + "text": null, + "value": null + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "datasource": { + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-ds", + "current": { + "text": null, + "value": null + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "csv": "1,2,3,4", + "scenarioId": "csv_metric_values" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-ds-empty-query", + "current": { + "text": null, + "value": null + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-default-ds", + "current": { + "text": null, + "value": null + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "prometheus", + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + } + ] + }, + "status": {} + } } } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v1beta1.json index a0cf65052a7..11151fc1345 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v1beta1.json @@ -9,7 +9,1055 @@ "conversion": { "failed": true, "error": "backend conversion not yet implemented", - "storedVersion": "v2alpha1" + "storedVersion": "v2alpha1", + "source": { + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "test-v2alpha1-annotations" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "grafana", + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "testdata-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "no-ds-testdata-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "prom-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "no-ds-prom-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "PBBCEC2D313BC06C3" + }, + "query": { + "kind": "grafana-postgresql-datasource", + "spec": { + "editorMode": "builder", + "format": "table", + "lines": 10, + "rawSql": "", + "refId": "Anno", + "scenarioId": "annotations", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "postgress-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "elasticsearch", + "uid": "gdev-elasticsearch" + }, + "query": { + "kind": "elasticsearch", + "spec": { + "lines": 10, + "query": "test query", + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "elastic - annos", + "builtIn": false, + "legacyOptions": { + "tagsField": "asd", + "textField": "asd", + "timeEndField": "asdas", + "timeField": "asd" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Simple timeseries (WITH DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "datasource": { + "uid": "gdev-testdata" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Simple stat (NO DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 4 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with NO REF to gdev-prometheus", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with ref to gdev-prometheus", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "datasource": { + "uid": "gdev-prometheus" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Mixed DS WITH REFS", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "datasource": { + "uid": "gdev-prometheus" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "datasource": { + "uid": "gdev-testdata" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Mixed DS WITHOUT REFS", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "maxColumnCount": 3, + "columnWidthMode": "standard", + "rowHeightMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with annotations", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "variable-ds-prometheus", + "current": { + "text": null, + "value": null + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "datasource": { + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-ds", + "current": { + "text": null, + "value": null + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "csv": "1,2,3,4", + "scenarioId": "csv_metric_values" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-ds-empty-query", + "current": { + "text": null, + "value": null + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-default-ds", + "current": { + "text": null, + "value": null + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "prometheus", + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + } + ] + }, + "status": {} + } } } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v0alpha1.json index add335f4b5e..ae50ed46c5d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v0alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v0alpha1.json @@ -9,7 +9,100 @@ "conversion": { "failed": true, "error": "backend conversion not yet implemented", - "storedVersion": "v2alpha1" + "storedVersion": "v2alpha1", + "source": { + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "test-v2alpha1-groupby-adhoc-vars" + }, + "spec": { + "annotations": null, + "cursorSync": "", + "elements": null, + "layout": null, + "links": null, + "preload": false, + "tags": null, + "timeSettings": { + "from": "", + "to": "", + "autoRefresh": "", + "autoRefreshIntervals": null, + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with group by and adhoc variables", + "variables": [ + { + "kind": "GroupByVariable", + "spec": { + "name": "", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "current": { + "text": "text7", + "value": "value7" + }, + "options": null, + "multi": false, + "label": "Group By Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "A group by variable" + } + }, + { + "kind": "AdhocVariable", + "spec": { + "name": "adhocVar", + "datasource": { + "type": "prometheus", + "uid": "datasource-3" + }, + "baseFilters": [ + { + "key": "key1", + "operator": "=", + "value": "value1", + "condition": "AND" + }, + { + "key": "key2", + "operator": "=", + "value": "value2", + "condition": "OR" + } + ], + "filters": [ + { + "key": "key3", + "operator": "=", + "value": "value3", + "condition": "AND" + } + ], + "defaultKeys": [ + { + "text": "defaultKey1", + "value": "defaultKey1", + "group": "defaultGroup1", + "expandable": true + } + ], + "label": "Adhoc Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "An adhoc variable", + "allowCustomValue": true + } + } + ] + }, + "status": {} + } } } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v1beta1.json index 4656a14bc7b..4f7ce909299 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v1beta1.json @@ -9,7 +9,100 @@ "conversion": { "failed": true, "error": "backend conversion not yet implemented", - "storedVersion": "v2alpha1" + "storedVersion": "v2alpha1", + "source": { + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "test-v2alpha1-groupby-adhoc-vars" + }, + "spec": { + "annotations": null, + "cursorSync": "", + "elements": null, + "layout": null, + "links": null, + "preload": false, + "tags": null, + "timeSettings": { + "from": "", + "to": "", + "autoRefresh": "", + "autoRefreshIntervals": null, + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with group by and adhoc variables", + "variables": [ + { + "kind": "GroupByVariable", + "spec": { + "name": "", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "current": { + "text": "text7", + "value": "value7" + }, + "options": null, + "multi": false, + "label": "Group By Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "A group by variable" + } + }, + { + "kind": "AdhocVariable", + "spec": { + "name": "adhocVar", + "datasource": { + "type": "prometheus", + "uid": "datasource-3" + }, + "baseFilters": [ + { + "key": "key1", + "operator": "=", + "value": "value1", + "condition": "AND" + }, + { + "key": "key2", + "operator": "=", + "value": "value2", + "condition": "OR" + } + ], + "filters": [ + { + "key": "key3", + "operator": "=", + "value": "value3", + "condition": "AND" + } + ], + "defaultKeys": [ + { + "text": "defaultKey1", + "value": "defaultKey1", + "group": "defaultGroup1", + "expandable": true + } + ], + "label": "Adhoc Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "An adhoc variable", + "allowCustomValue": true + } + } + ] + }, + "status": {} + } } } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v0alpha1.json index 86e7f731622..acde79a0bcc 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v0alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v0alpha1.json @@ -9,7 +9,219 @@ "conversion": { "failed": true, "error": "backend conversion not yet implemented", - "storedVersion": "v2alpha1" + "storedVersion": "v2alpha1", + "source": { + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "test-v2alpha1-viz-config" + }, + "spec": { + "annotations": null, + "cursorSync": "", + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Simple timeseries (WITH DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "datasource": { + "uid": "gdev-testdata" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "maxColumnCount": 3, + "columnWidthMode": "standard", + "rowHeightMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with viz config", + "variables": null + }, + "status": {} + } } } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v1beta1.json index cc538fe2f92..e0812fa1a2b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v1beta1.json @@ -9,7 +9,219 @@ "conversion": { "failed": true, "error": "backend conversion not yet implemented", - "storedVersion": "v2alpha1" + "storedVersion": "v2alpha1", + "source": { + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "test-v2alpha1-viz-config" + }, + "spec": { + "annotations": null, + "cursorSync": "", + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Simple timeseries (WITH DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "datasource": { + "uid": "gdev-testdata" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "maxColumnCount": 3, + "columnWidthMode": "standard", + "rowHeightMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with viz config", + "variables": null + }, + "status": {} + } } } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/v0.go b/apps/dashboard/pkg/migration/conversion/v0.go index a1ccc0e5a6c..9eb42167072 100644 --- a/apps/dashboard/pkg/migration/conversion/v0.go +++ b/apps/dashboard/pkg/migration/conversion/v0.go @@ -78,6 +78,7 @@ func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, s StoredVersion: ptr.To(dashv0.VERSION), Failed: true, Error: ptr.To("backend conversion not yet implemented"), + Source: in, }, } @@ -94,6 +95,7 @@ func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, sco StoredVersion: ptr.To(dashv0.VERSION), Failed: true, Error: ptr.To("backend conversion not yet implemented"), + Source: in, }, } diff --git a/apps/dashboard/pkg/migration/conversion/v1.go b/apps/dashboard/pkg/migration/conversion/v1.go index 2dd51ffff03..86b3827af27 100644 --- a/apps/dashboard/pkg/migration/conversion/v1.go +++ b/apps/dashboard/pkg/migration/conversion/v1.go @@ -49,6 +49,7 @@ func Convert_V1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, s StoredVersion: ptr.To(dashv1.VERSION), Failed: true, Error: ptr.To("backend conversion not yet implemented"), + Source: in, }, } @@ -65,6 +66,7 @@ func Convert_V1_to_V2beta1(in *dashv1.Dashboard, out *dashv2beta1.Dashboard, sco StoredVersion: ptr.To(dashv1.VERSION), Failed: true, Error: ptr.To("backend conversion not yet implemented"), + Source: in, }, } diff --git a/apps/dashboard/pkg/migration/conversion/v2.go b/apps/dashboard/pkg/migration/conversion/v2.go index a7dfb9f39fd..91fcd8cf07a 100644 --- a/apps/dashboard/pkg/migration/conversion/v2.go +++ b/apps/dashboard/pkg/migration/conversion/v2.go @@ -20,6 +20,7 @@ func Convert_V2alpha1_to_V0(in *dashv2alpha1.Dashboard, out *dashv0.Dashboard, s StoredVersion: ptr.To(dashv2alpha1.VERSION), Failed: true, Error: ptr.To("backend conversion not yet implemented"), + Source: in, }, } @@ -36,6 +37,7 @@ func Convert_V2alpha1_to_V1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, s StoredVersion: ptr.To(dashv2alpha1.VERSION), Failed: true, Error: ptr.To("backend conversion not yet implemented"), + Source: in, }, } @@ -52,6 +54,7 @@ func Convert_V2alpha1_to_V2beta1(in *dashv2alpha1.Dashboard, out *dashv2beta1.Da StoredVersion: ptr.To(dashv2alpha1.VERSION), Failed: true, Error: ptr.To(err.Error()), + Source: in, }, } @@ -79,6 +82,7 @@ func Convert_V2beta1_to_V0(in *dashv2beta1.Dashboard, out *dashv0.Dashboard, sco StoredVersion: ptr.To(dashv2beta1.VERSION), Failed: true, Error: ptr.To("backend conversion not yet implemented"), + Source: in, }, } @@ -95,6 +99,7 @@ func Convert_V2beta1_to_V1(in *dashv2beta1.Dashboard, out *dashv1.Dashboard, sco StoredVersion: ptr.To(dashv2beta1.VERSION), Failed: true, Error: ptr.To("backend conversion not yet implemented"), + Source: in, }, } @@ -111,6 +116,7 @@ func Convert_V2beta1_to_V2alpha1(in *dashv2beta1.Dashboard, out *dashv2alpha1.Da StoredVersion: ptr.To(dashv2beta1.VERSION), Failed: true, Error: ptr.To("backend conversion not yet implemented"), + Source: in, }, } diff --git a/packages/grafana-schema/src/schema/dashboard/v0alpha1/types.status.gen.ts b/packages/grafana-schema/src/schema/dashboard/v0alpha1/types.status.gen.ts index 91bf7909fd3..978b71f39b3 100644 --- a/packages/grafana-schema/src/schema/dashboard/v0alpha1/types.status.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v0alpha1/types.status.gen.ts @@ -12,6 +12,8 @@ export interface ConversionStatus { // The version which was stored when the dashboard was created / updated. // Fetching this version should always succeed. storedVersion?: string; + // The original value map[string]any + source?: any; } export const defaultConversionStatus = (): ConversionStatus => ({ diff --git a/packages/grafana-schema/src/schema/dashboard/v1beta1/types.status.gen.ts b/packages/grafana-schema/src/schema/dashboard/v1beta1/types.status.gen.ts index 91bf7909fd3..978b71f39b3 100644 --- a/packages/grafana-schema/src/schema/dashboard/v1beta1/types.status.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v1beta1/types.status.gen.ts @@ -12,6 +12,8 @@ export interface ConversionStatus { // The version which was stored when the dashboard was created / updated. // Fetching this version should always succeed. storedVersion?: string; + // The original value map[string]any + source?: any; } export const defaultConversionStatus = (): ConversionStatus => ({ diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.status.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.status.gen.ts index 91bf7909fd3..978b71f39b3 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.status.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.status.gen.ts @@ -12,6 +12,8 @@ export interface ConversionStatus { // The version which was stored when the dashboard was created / updated. // Fetching this version should always succeed. storedVersion?: string; + // The original value map[string]any + source?: any; } export const defaultConversionStatus = (): ConversionStatus => ({ diff --git a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.status.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.status.gen.ts index 91bf7909fd3..978b71f39b3 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.status.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.status.gen.ts @@ -12,6 +12,8 @@ export interface ConversionStatus { // The version which was stored when the dashboard was created / updated. // Fetching this version should always succeed. storedVersion?: string; + // The original value map[string]any + source?: any; } export const defaultConversionStatus = (): ConversionStatus => ({ diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json index c9ba9823986..e68f94dda3e 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json @@ -2381,6 +2381,10 @@ "type": "boolean", "default": false }, + "source": { + "description": "The original value map[string]any", + "type": "object" + }, "storedVersion": { "description": "The version which was stored when the dashboard was created / updated. Fetching this version should always succeed.", "type": "string" diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v1beta1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v1beta1.json index f025701ec73..4ae094ff867 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v1beta1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v1beta1.json @@ -1075,6 +1075,10 @@ "type": "boolean", "default": false }, + "source": { + "description": "The original value map[string]any", + "type": "object" + }, "storedVersion": { "description": "The version which was stored when the dashboard was created / updated. Fetching this version should always succeed.", "type": "string" diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json index ddc33887775..d8962a72a5d 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -1732,6 +1732,10 @@ "type": "boolean", "default": false }, + "source": { + "description": "The original value map[string]any", + "type": "object" + }, "storedVersion": { "description": "The version which was stored when the dashboard was created / updated. Fetching this version should always succeed.", "type": "string" From 6059ef3d83a235ed47f5d81e5663e662e7c9b340 Mon Sep 17 00:00:00 2001 From: Ihor Yeromin Date: Fri, 10 Oct 2025 14:51:45 +0200 Subject: [PATCH 126/578] Fix: Sorting with sparse arrays containing empty values (#112114) --- .../src/dataframe/processDataFrame.test.ts | 12 ++++++++++++ .../src/dataframe/processDataFrame.ts | 16 +++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/grafana-data/src/dataframe/processDataFrame.test.ts b/packages/grafana-data/src/dataframe/processDataFrame.test.ts index 72b3041d87c..5c7593e91c0 100644 --- a/packages/grafana-data/src/dataframe/processDataFrame.test.ts +++ b/packages/grafana-data/src/dataframe/processDataFrame.test.ts @@ -365,6 +365,7 @@ describe('sorted DataFrame', () => { { name: 'fourth', type: FieldType.time, values: [1, 2, 3], nanos: [10, 20, 30] }, ], }); + it('Should sort numbers', () => { const sorted = sortDataFrame(frame, 0, true); expect(sorted.length).toEqual(3); @@ -386,6 +387,17 @@ describe('sorted DataFrame', () => { expect(sorted.fields[3].values).toEqual([3, 2, 1]); expect(sorted.fields[3].nanos).toEqual([30, 20, 10]); }); + + it('Should handle arrays with empty values correctly', () => { + // Create a sparse array with empty slots (undefined values) + const values = ['502', '502', , '500', '500', '200', '404']; // Note the empty slot at index 2 + const frame = toDataFrame({ + fields: [{ name: 'status', type: FieldType.string, values }], + }); + const sorted = sortDataFrame(frame, 0, false); + + expect(sorted.fields[0].values).toEqual(['200', '404', '500', '500', '502', '502', undefined]); + }); }); describe('sorted DataFrame by nanos', () => { diff --git a/packages/grafana-data/src/dataframe/processDataFrame.ts b/packages/grafana-data/src/dataframe/processDataFrame.ts index 4a028b6b4fd..14621547725 100644 --- a/packages/grafana-data/src/dataframe/processDataFrame.ts +++ b/packages/grafana-data/src/dataframe/processDataFrame.ts @@ -429,18 +429,20 @@ export function sortDataFrame(data: DataFrame, sortIndex?: number, reverse = fal return { ...data, - fields: data.fields.map((f) => { - const newF = { - ...f, - values: f.values.map((v, i) => f.values[index[i]]), + fields: data.fields.map((field) => { + const newValues = Array.from({ length: field.values.length }, (_, i) => field.values[index[i]]); + + const newField = { + ...field, + values: newValues, }; // only add .nanos if it exists - const { nanos } = f; + const { nanos } = field; if (nanos !== undefined) { - newF.nanos = nanos.map((n, i) => nanos[index[i]]); + newField.nanos = Array.from({ length: nanos.length }, (_, i) => nanos[index[i]]); } - return newF; + return newField; }), }; } From cb9118627693d41bcffdc4558a416c06a4ff0935 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Fri, 10 Oct 2025 16:20:48 +0300 Subject: [PATCH 127/578] GenAI: Fix uncaught error when panel title is missing (#112277) --- public/app/features/dashboard/components/GenAI/utils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/public/app/features/dashboard/components/GenAI/utils.ts b/public/app/features/dashboard/components/GenAI/utils.ts index ffc3740b57c..ea1a8fe425b 100644 --- a/public/app/features/dashboard/components/GenAI/utils.ts +++ b/public/app/features/dashboard/components/GenAI/utils.ts @@ -164,9 +164,7 @@ export const DASHBOARD_NEED_PANEL_TITLES_AND_DESCRIPTIONS_MESSAGE = export function getPanelStrings(dashboard: DashboardModel): string[] { const panelStrings = dashboard.panels .filter( - (panel) => - (panel.title.length > 0 && panel.title !== NEW_PANEL_TITLE) || - (panel.description && panel.description.length > 0) + (panel) => (panel.title && panel.title !== NEW_PANEL_TITLE) || (panel.description && panel.description.length > 0) ) .map(getPanelString); From f4e7dfe82727d1050a47cb522774a73d6d89d619 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Fri, 10 Oct 2025 16:36:37 +0300 Subject: [PATCH 128/578] Dashboard: Improve static options editors on variables (#110831) --- .../dashboards-edit-custom-variables.spec.ts | 212 ++++++++++++++++++ .../dashboards-edit-variables.spec.ts | 40 ---- eslint-suppressions.json | 13 -- package.json | 4 +- .../src/selectors/pages.ts | 41 ++-- .../components/CustomVariableForm.tsx | 47 +--- .../components/QueryVariableForm.test.tsx | 20 +- .../components/VariableOptionsInput.tsx | 106 --------- .../components/VariableStaticOptionsForm.tsx | 139 ++++++++++++ .../VariableStaticOptionsFormAddButton.tsx | 21 ++ .../VariableStaticOptionsFormItemEditor.tsx | 117 ++++++++++ .../VariableStaticOptionsFormItems.tsx | 117 ++++++++++ .../editors/CustomVariableEditor.tsx | 89 -------- .../CustomVariableEditor.test.tsx | 0 .../CustomVariableEditor.tsx | 83 +++++++ .../CustomVariableEditor/ModalEditor.tsx | 47 ++++ .../editors/CustomVariableEditor/PaneItem.tsx | 37 +++ .../CustomVariableEditor/ValuesBuilder.tsx | 52 +++++ .../CustomVariableEditor/ValuesPreview.tsx | 13 ++ .../getCustomVariableOptions.tsx | 20 ++ .../editors/QueryVariableEditor.test.tsx | 12 +- .../variables/editors/QueryVariableEditor.tsx | 32 ++- .../settings/variables/utils.test.ts | 2 +- .../settings/variables/utils.ts | 6 +- .../query/QueryVariableStaticOptions.tsx | 8 +- public/locales/en-US/grafana.json | 20 +- yarn.lock | 22 +- 27 files changed, 966 insertions(+), 354 deletions(-) create mode 100644 e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts delete mode 100644 public/app/features/dashboard-scene/settings/variables/components/VariableOptionsInput.tsx create mode 100644 public/app/features/dashboard-scene/settings/variables/components/VariableStaticOptionsForm.tsx create mode 100644 public/app/features/dashboard-scene/settings/variables/components/VariableStaticOptionsFormAddButton.tsx create mode 100644 public/app/features/dashboard-scene/settings/variables/components/VariableStaticOptionsFormItemEditor.tsx create mode 100644 public/app/features/dashboard-scene/settings/variables/components/VariableStaticOptionsFormItems.tsx delete mode 100644 public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor.tsx rename public/app/features/dashboard-scene/settings/variables/editors/{ => CustomVariableEditor}/CustomVariableEditor.test.tsx (100%) create mode 100644 public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/CustomVariableEditor.tsx create mode 100644 public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ModalEditor.tsx create mode 100644 public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/PaneItem.tsx create mode 100644 public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesBuilder.tsx create mode 100644 public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesPreview.tsx create mode 100644 public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/getCustomVariableOptions.tsx diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts new file mode 100644 index 00000000000..ce5465c1e33 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts @@ -0,0 +1,212 @@ +import { Locator } from '@playwright/test'; + +import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +import { flows } from './utils'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + dashboardUndoRedo: true, + groupByVariable: true, + }, +}); + +test.use({ + viewport: { width: 1920, height: 1080 }, +}); + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Dashboard edit - Custom variable', + { + tag: ['@dashboards'], + }, + () => { + let addButton: Locator | undefined; + let rows: Locator | undefined; + let valueInputs: Locator | undefined; + let labelInputs: Locator | undefined; + let deleteButtons: Locator | undefined; + + const getAddButton = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => { + addButton = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.addButton + ); + await expect(addButton).toBeVisible(); + }; + + const refetchItems = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => { + rows = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.row + ); + + valueInputs = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.valueInput + ); + + labelInputs = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.labelInput + ); + + deleteButtons = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.deleteButton + ); + }; + + const checkRows = async (length: number) => { + expect(await rows!.all()).toHaveLength(length); + }; + + const fillValue = async (text: string, index: number) => { + await valueInputs!.nth(index).fill(text); + }; + + const fillLabel = async (text: string, index: number) => { + await labelInputs!.nth(index).fill(text); + }; + + const fillLabelValue = async (value: string, label: string, index: number) => { + await fillValue(value, index); + await fillLabel(label, index); + }; + + const openModal = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => { + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.optionsOpenButton) + .click(); + + await getAddButton(dashboardPage, selectors); + + refetchItems(dashboardPage, selectors); + }; + + const closeModal = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => { + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.closeButton) + .click(); + }; + + const checkItems = async (items: Array<[string, string?]>) => { + for (let i = 0; i < items.length; i++) { + const [value, label] = items[i]; + await expect(valueInputs!.nth(i)).toHaveValue(value); + await expect(labelInputs!.nth(i)).toHaveValue(label ?? ''); + } + }; + + const checkPreview = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups, labels: string[]) => { + const previewOptions = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption + ); + + for (let i = 0; i < labels.length; i++) { + expect(await previewOptions.nth(i).textContent()).toBe(labels[i]); + } + }; + + const addItem = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups, value = '', label = '') => { + await addButton!.click(); + refetchItems(dashboardPage, selectors); + await fillLabelValue(value ?? '', label ?? '', (await rows!.all()).length - 1); + }; + + const removeItem = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups, index: number) => { + await deleteButtons!.nth(index).click(); + refetchItems(dashboardPage, selectors); + }; + + test.beforeEach(() => { + valueInputs = undefined; + labelInputs = undefined; + deleteButtons = undefined; + }); + + test('can add a new custom variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + // common steps to add a new variable + await flows.newEditPaneVariableClick(dashboardPage, selectors); + await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, { + type: 'custom', + name: 'foo', + label: 'Foo', + value: '', + }); + + await openModal(dashboardPage, selectors); + await checkRows(1); + await addItem(dashboardPage, selectors); + await checkRows(2); + await fillValue('first value', 0); + await fillLabelValue('second value', 'second label', 1); + await addItem(dashboardPage, selectors, 'third value', 'third label'); + await addItem(dashboardPage, selectors, 'fourth value', 'fourth value'); + await removeItem(dashboardPage, selectors, 2); + await checkRows(3); + await checkPreview(dashboardPage, selectors, ['first value', 'second label', 'fourth value']); + await closeModal(dashboardPage, selectors); + + // assert variable is visible and has the correct values + const variableLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels('Foo') + ); + await expect(variableLabel).toBeVisible(); + await expect(variableLabel).toContainText('Foo'); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('first value') + ) + ).toBeVisible(); + + // check that variable deletion works + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click(); + await expect(variableLabel).toBeHidden(); + }); + + test('can edit a custom variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + // Create a custom variable in the dashboard settings page + await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click(); + const typeSelect = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2) + .locator('input'); + await typeSelect.fill('Custom'); + await typeSelect.press('Enter'); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2) + .fill('foo'); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2) + .fill('Foo'); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput) + .fill('first value, second label : second value, fourth value : fourth value'); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + + // Open the modal editor in the side pane + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.section).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.node('Variables')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('foo')).click(); + await openModal(dashboardPage, selectors); + + // Check the items + await checkItems([['first value'], ['second value', 'second label'], ['fourth value']]); + await checkPreview(dashboardPage, selectors, ['first value', 'second label', 'fourth value']); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-variables.spec.ts index 54991dfd623..4b5ea9574f8 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-edit-variables.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-variables.spec.ts @@ -20,46 +20,6 @@ test.describe( tag: ['@dashboards'], }, () => { - test('can add a new custom variable', async ({ gotoDashboardPage, selectors, page }) => { - const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); - await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); - - const variable: Variable = { - type: 'custom', - name: 'foo', - label: 'Foo', - value: 'one,two,three', - }; - - // common steps to add a new variable - await flows.newEditPaneVariableClick(dashboardPage, selectors); - await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable); - - // set the custom variable value - const customValueInput = dashboardPage.getByGrafanaSelector( - selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput - ); - await customValueInput.fill(variable.value); - await customValueInput.blur(); - - // assert the dropdown for the variable is visible and has the correct values - const variableLabel = dashboardPage.getByGrafanaSelector( - selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!) - ); - await expect(variableLabel).toBeVisible(); - await expect(variableLabel).toContainText(variable.label!); - - const values = variable.value.split(','); - const firstValueLink = dashboardPage.getByGrafanaSelector( - selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(values[0]) - ); - await expect(firstValueLink).toBeVisible(); - - // check that variable deletion works - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click(); - await expect(variableLabel).toBeHidden(); - }); - test('can add a new constant variable', async ({ gotoDashboardPage, selectors, page }) => { const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 1d3a1535b9b..cb9a72a644b 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -2189,19 +2189,6 @@ "count": 1 } }, - "public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, - "public/app/features/dashboard-scene/settings/variables/utils.ts": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - }, - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, "public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/ConfigEmailSharing/ConfigEmailSharing.tsx": { "no-restricted-syntax": { "count": 1 diff --git a/package.json b/package.json index d8fe5451d99..bce42eab31e 100644 --- a/package.json +++ b/package.json @@ -296,8 +296,8 @@ "@grafana/plugin-ui": "^0.10.10", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "6.39.3", - "@grafana/scenes-react": "6.39.3", + "@grafana/scenes": "6.39.4", + "@grafana/scenes-react": "6.39.4", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index b45357f05f7..7716ccc1436 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -500,24 +500,9 @@ export const versionedPages = { queryOptionsQueryInput: { '10.4.0': 'data-testid Variable editor Form Default Variable Query Editor textarea', }, - queryOptionsStaticOptionsRow: { - [MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options row', - }, queryOptionsStaticOptionsToggle: { [MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options toggle', }, - queryOptionsStaticOptionsLabelInput: { - [MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Label input', - }, - queryOptionsStaticOptionsValueInput: { - [MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Value input', - }, - queryOptionsStaticOptionsDeleteButton: { - [MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Delete button', - }, - queryOptionsStaticOptionsAddButton: { - [MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Add button', - }, queryOptionsStaticOptionsOrderDropdown: { [MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Order dropdown', }, @@ -559,6 +544,12 @@ export const versionedPages = { customValueInput: { [MIN_GRAFANA_VERSION]: 'data-testid custom-variable-input', }, + optionsOpenButton: { + [MIN_GRAFANA_VERSION]: 'data-testid custom-variable-options-open-button', + }, + closeButton: { + [MIN_GRAFANA_VERSION]: 'data-testid custom-variable-close-button', + }, }, IntervalVariable: { intervalsValueInput: { @@ -607,6 +598,26 @@ export const versionedPages = { ['12.3.0']: 'data-testid switch variable disabled value input', }, }, + StaticOptionsEditor: { + addButton: { + [MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Static Options Add button', + }, + labelInput: { + [MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Static Options Label input', + }, + valueInput: { + [MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Static Options Value input', + }, + moveButton: { + [MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Static Options Move button', + }, + deleteButton: { + [MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Static Options Delete button', + }, + row: { + [MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Static Options Row', + }, + }, }, }, }, diff --git a/public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.tsx index f11037fb80c..b3c78330156 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.tsx @@ -1,16 +1,11 @@ import { FormEvent } from 'react'; -import { lastValueFrom } from 'rxjs'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; -import { CustomVariable, SceneVariable } from '@grafana/scenes'; -import { TextArea } from '@grafana/ui'; -import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; - -import { VariableLegend } from '../components/VariableLegend'; -import { VariableTextAreaField } from '../components/VariableTextAreaField'; import { SelectionOptionsForm } from './SelectionOptionsForm'; +import { VariableLegend } from './VariableLegend'; +import { VariableTextAreaField } from './VariableTextAreaField'; interface CustomVariableFormProps { query: string; @@ -71,41 +66,3 @@ export function CustomVariableForm({ ); } - -export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneItemDescriptor[] { - if (!(variable instanceof CustomVariable)) { - return []; - } - - return [ - new OptionsPaneItemDescriptor({ - title: t('dashboard.edit-pane.variable.custom-options.values', 'Values separated by comma'), - id: 'custom-variable-values', - render: (descriptor) => , - }), - ]; -} - -function ValuesTextField({ variable, id }: { variable: CustomVariable; id?: string }) { - const { query } = variable.useState(); - - const onBlur = async (event: FormEvent) => { - variable.setState({ query: event.currentTarget.value }); - await lastValueFrom(variable.validateAndUpdate!()); - }; - - return ( -

    75lwXbN61_LnP27m`UGA*gZSO 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%0D@tMIU>@i6&2dq6?C=_O!xliDHE&p$v2 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 N_qpu7x!dQ5e*$7@7yJMK literal 0 HcmV?d00001 diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index ed29beb6afe..ed0738c373a 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -4142,6 +4142,13 @@ "body": "Try the new Advisor to uncover potential issues with your data sources and plugins.", "go-to-advisor": "Go to Advisor" }, + "cache-feature-highlight-page": { + "header": "Query caching can improve load times and reduce API costs by temporarily storing the results of data source queries. When you or other users submit the same query, the results will come back from the cache instead of from the data source.", + "item-1": "Faster dashboard load times, especially for popular dashboards.", + "item-2": "Reduced API costs.", + "item-3": "Reduced likelihood that APIs will rate-limit or throttle requests.", + "title": "Optimize queries with Query Caching in Grafana Cloud" + }, "cloud": { "connections-home-page": { "add-new-connection": { @@ -4190,6 +4197,20 @@ "unknown-datasource": "Unknown datasource" } }, + "feature-highlight-page": { + "foot-note": "After creating an account, you can easily <2>migrate this instance to Grafana Cloud with our Migration Assistant.", + "footer": "Create a Grafana Cloud Free account to start using data source permissions. This feature is also available with a Grafana Enterprise license.", + "footer-link": "Learn about Enterprise", + "link-button-label": "Create account" + }, + "insights-feature-highlight-page": { + "header": "Usage Insights provide detailed information about data source usage, like the number of views, queries, and errors users have experienced. You can use this to improve users’ experience and troubleshoot issues.", + "item-1": "Demonstrate and improve the value of your observability service by keeping track of user engagement", + "item-2": "Keep Grafana performant and by identifying and fixing slow, error-prone data sources", + "item-3": "Clean up your instance by finding and removing unused data sources", + "item-4": "Review individual data source usage insights at a glance in the UI, sort search results by usage and errors, or dig into detailed usage logs", + "title": "Understand usage of your data sources with Grafana Cloud" + }, "new-data-source-page": { "subTitle": { "choose-a-data-source-type": "Choose a data source type" @@ -4220,6 +4241,13 @@ "subtitle": "Manage your data source connections in one place. Use this page to add a new data source or manage your existing connections." } }, + "permissions-feature-highlight-page": { + "header": "With data source permissions, you can protect sensitive data by limiting access to this data source to specific users, teams, and roles.", + "item-1": "Protect sensitive data, like security logs, production databases, and personally-identifiable information", + "item-2": "Clean up users’ experience by hiding data sources they don’t need to use", + "item-3": "Share Grafana access more freely, knowing that users will not unwittingly see sensitive data", + "title": "Secure access to data with data source permissions in Grafana Cloud" + }, "search": { "aria-label-search-all": "Search all", "placeholder": "Search all" From 7e63a01a798d0f04e509c656943deca276910e75 Mon Sep 17 00:00:00 2001 From: Tito Lins Date: Mon, 6 Oct 2025 14:23:21 +0200 Subject: [PATCH 013/578] alerting: omit optional notification settings fields (#112049) --- pkg/services/ngalert/api/api_provisioning.go | 21 ++++- .../ngalert/api/api_ruler_export_test.go | 27 ++++++ pkg/services/ngalert/api/compat/compat.go | 6 +- ...st-rulegroup-simplified-routing-export.hcl | 45 +++++++++ .../post-rulegroup-simplified-routing.json | 94 +++++++++++++++++++ .../definitions/provisioning_alert_rules.go | 14 +-- 6 files changed, 192 insertions(+), 15 deletions(-) create mode 100644 pkg/services/ngalert/api/test-data/post-rulegroup-simplified-routing-export.hcl create mode 100644 pkg/services/ngalert/api/test-data/post-rulegroup-simplified-routing.json diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 29f3ca6679b..dc8863e1fb6 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -525,7 +525,7 @@ func determineProvenance(ctx *contextmodel.ReqContext) definitions.Provenance { } func extractExportRequest(c *contextmodel.ReqContext) definitions.ExportQueryParams { - var format = "yaml" + format := "yaml" acceptHeader := c.Req.Header.Get("Accept") if strings.Contains(acceptHeader, "yaml") { @@ -673,11 +673,22 @@ func escapeRuleGroup(group definitions.AlertRuleGroupExport) definitions.AlertRu func escapeRuleNotificationSettings(ns definitions.AlertRuleNotificationSettingsExport) definitions.AlertRuleNotificationSettingsExport { ns.Receiver = addEscapeCharactersToString(ns.Receiver) - for j := range ns.GroupBy { - ns.GroupBy[j] = addEscapeCharactersToString(ns.GroupBy[j]) + if ns.GroupBy != nil { + for j := range *ns.GroupBy { + (*ns.GroupBy)[j] = addEscapeCharactersToString((*ns.GroupBy)[j]) + } } - for k := range ns.MuteTimeIntervals { - ns.MuteTimeIntervals[k] = addEscapeCharactersToString(ns.MuteTimeIntervals[k]) + + if ns.MuteTimeIntervals != nil { + for k := range *ns.MuteTimeIntervals { + (*ns.MuteTimeIntervals)[k] = addEscapeCharactersToString((*ns.MuteTimeIntervals)[k]) + } + } + + if ns.ActiveTimeIntervals != nil { + for k := range *ns.ActiveTimeIntervals { + (*ns.ActiveTimeIntervals)[k] = addEscapeCharactersToString((*ns.ActiveTimeIntervals)[k]) + } } return ns } diff --git a/pkg/services/ngalert/api/api_ruler_export_test.go b/pkg/services/ngalert/api/api_ruler_export_test.go index d9b69d9e1d3..dcdeaf0c436 100644 --- a/pkg/services/ngalert/api/api_ruler_export_test.go +++ b/pkg/services/ngalert/api/api_ruler_export_test.go @@ -201,6 +201,33 @@ func TestExportFromPayload(t *testing.T) { require.Equal(t, `attachment;filename=export.tf`, rc.Resp.Header().Get("Content-Disposition")) }) }) + + t.Run("hcl body with simplified routing is as expected", func(t *testing.T) { + requestFile := "post-rulegroup-simplified-routing.json" + + rawBody, err := testData.ReadFile(path.Join("test-data", requestFile)) + require.NoError(t, err) + + var buf bytes.Buffer + require.NoError(t, json.Compact(&buf, rawBody)) + + var body apimodels.PostableRuleGroupConfig + require.NoError(t, json.Unmarshal(buf.Bytes(), &body)) + + expectedResponse, err := testData.ReadFile(path.Join("test-data", strings.Replace(requestFile, ".json", "-export.hcl", 1))) + require.NoError(t, err) + + rc := createRequest() + rc.Req.Form.Set("format", "hcl") + rc.Req.Form.Set("download", "false") + + response := srv.ExportFromPayload(rc, body, folder.UID) + response.WriteTo(rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, string(expectedResponse), string(response.Body())) + require.Equal(t, "text/hcl", rc.Resp.Header().Get("Content-Type")) + }) } func TestExportRules(t *testing.T) { diff --git a/pkg/services/ngalert/api/compat/compat.go b/pkg/services/ngalert/api/compat/compat.go index 9d1796f78f5..eadf40217af 100644 --- a/pkg/services/ngalert/api/compat/compat.go +++ b/pkg/services/ngalert/api/compat/compat.go @@ -483,12 +483,12 @@ func AlertRuleNotificationSettingsExportFromNotificationSettings(ns []models.Not return &definitions.AlertRuleNotificationSettingsExport{ Receiver: m.Receiver, - GroupBy: m.GroupBy, + GroupBy: NilIfEmpty(util.Pointer(m.GroupBy)), GroupWait: toStringIfNotNil(m.GroupWait), GroupInterval: toStringIfNotNil(m.GroupInterval), RepeatInterval: toStringIfNotNil(m.RepeatInterval), - MuteTimeIntervals: m.MuteTimeIntervals, - ActiveTimeIntervals: m.ActiveTimeIntervals, + MuteTimeIntervals: NilIfEmpty(util.Pointer(m.MuteTimeIntervals)), + ActiveTimeIntervals: NilIfEmpty(util.Pointer(m.ActiveTimeIntervals)), } } diff --git a/pkg/services/ngalert/api/test-data/post-rulegroup-simplified-routing-export.hcl b/pkg/services/ngalert/api/test-data/post-rulegroup-simplified-routing-export.hcl new file mode 100644 index 00000000000..ff3c07ee97e --- /dev/null +++ b/pkg/services/ngalert/api/test-data/post-rulegroup-simplified-routing-export.hcl @@ -0,0 +1,45 @@ +resource "grafana_rule_group" "rule_group_2b12784d0e1454cd" { + org_id = 1 + name = "group_simplified_routing" + folder_uid = "e4584834-1a87-4dff-8913-8a4748dfca79" + interval_seconds = 10 + + rule { + name = "test" + condition = "C" + + data { + ref_id = "A" + + relative_time_range { + from = 600 + to = 0 + } + + datasource_uid = "grafanacloud-prom" + model = "{\"editorMode\":\"code\",\"expr\":\"vector(1)\",\"instant\":true,\"intervalMs\":1000,\"legendFormat\":\"__auto\",\"maxDataPoints\":43200,\"range\":false,\"refId\":\"A\"}" + } + data { + ref_id = "C" + + relative_time_range { + from = 0 + to = 0 + } + + datasource_uid = "__expr__" + model = "{\"conditions\":[{\"evaluator\":{\"params\":[1],\"type\":\"gt\"},\"operator\":{\"type\":\"and\"},\"query\":{\"params\":[\"C\"]},\"reducer\":{\"params\":[],\"type\":\"last\"},\"type\":\"query\"}],\"datasource\":{\"type\":\"__expr__\",\"uid\":\"__expr__\"},\"expression\":\"A\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"C\",\"type\":\"threshold\"}" + } + + no_data_state = "NoData" + exec_err_state = "Error" + for = "1m" + annotations = {} + labels = {} + is_paused = false + + notification_settings { + contact_point = "email" + } + } +} diff --git a/pkg/services/ngalert/api/test-data/post-rulegroup-simplified-routing.json b/pkg/services/ngalert/api/test-data/post-rulegroup-simplified-routing.json new file mode 100644 index 00000000000..7cd163d4d35 --- /dev/null +++ b/pkg/services/ngalert/api/test-data/post-rulegroup-simplified-routing.json @@ -0,0 +1,94 @@ +{ + "name": "group_simplified_routing", + "interval": "10s", + "rules": [ + { + "grafana_alert": { + "title": "test", + "condition": "C", + "data": [ + { + "refId": "A", + "queryType": "", + "relativeTimeRange": { + "from": 600, + "to": 0 + }, + "datasourceUid": "grafanacloud-prom", + "model": { + "editorMode": "code", + "expr": "vector(1)", + "instant": true, + "intervalMs": 1000, + "legendFormat": "__auto", + "maxDataPoints": 43200, + "range": false, + "refId": "A" + } + }, + { + "refId": "C", + "queryType": "", + "relativeTimeRange": { + "from": 0, + "to": 0 + }, + "datasourceUid": "__expr__", + "model": { + "conditions": [ + { + "evaluator": { + "params": [ + 1 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "C" + ] + }, + "reducer": { + "params": [], + "type": "last" + }, + "type": "query" + } + ], + "datasource": { + "type": "__expr__", + "uid": "__expr__" + }, + "expression": "A", + "intervalMs": 1000, + "maxDataPoints": 43200, + "refId": "C", + "type": "threshold" + } + } + ], + "is_paused": false, + "no_data_state": "NoData", + "exec_err_state": "Error", + "notification_settings": { + "receiver": "email" + }, + "metadata": { + "editor_settings": { + "simplified_query_and_expressions_section": false, + "simplified_notifications_section": true + } + }, + "missing_series_evals_to_resolve": 0, + "uid": "alert-with-simplified-routing" + }, + "annotations": {}, + "labels": {}, + "for": "1m", + "keep_firing_for": "0s" + } + ] +} 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 0d91080064b..cbfb7adfd24 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go @@ -306,13 +306,13 @@ type RelativeTimeRangeExport struct { type AlertRuleNotificationSettingsExport struct { // Field name mismatches with Terraform provider schema are noted where applicable. - Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty" hcl:"contact_point"` // TF -> `contact_point` - GroupBy []string `yaml:"group_by,omitempty" json:"group_by,omitempty" hcl:"group_by"` - GroupWait *string `yaml:"group_wait,omitempty" json:"group_wait,omitempty" hcl:"group_wait,optional"` - GroupInterval *string `yaml:"group_interval,omitempty" json:"group_interval,omitempty" hcl:"group_interval,optional"` - RepeatInterval *string `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty" hcl:"repeat_interval,optional"` - MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty" hcl:"mute_timings"` // TF -> `mute_timings` - ActiveTimeIntervals []string `yaml:"active_time_intervals,omitempty" json:"active_time_intervals,omitempty" hcl:"active_timings"` // TF -> `active_timings` + Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty" hcl:"contact_point"` // TF -> `contact_point` + GroupBy *[]string `yaml:"group_by,omitempty" json:"group_by,omitempty" hcl:"group_by,optional"` + GroupWait *string `yaml:"group_wait,omitempty" json:"group_wait,omitempty" hcl:"group_wait,optional"` + GroupInterval *string `yaml:"group_interval,omitempty" json:"group_interval,omitempty" hcl:"group_interval,optional"` + RepeatInterval *string `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty" hcl:"repeat_interval,optional"` + MuteTimeIntervals *[]string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty" hcl:"mute_timings,optional"` // TF -> `mute_timings` + ActiveTimeIntervals *[]string `yaml:"active_time_intervals,omitempty" json:"active_time_intervals,omitempty" hcl:"active_timings,optional"` // TF -> `active_timings` } // Record is the provisioned export of models.Record. From a6f92d44877d84c68e3e00e99c7da3c93fdb1ea5 Mon Sep 17 00:00:00 2001 From: Michael Lamb Date: Mon, 6 Oct 2025 07:33:12 -0500 Subject: [PATCH 014/578] Update index.md (#112028) --- .../share-dashboards-panels/shared-dashboards/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/dashboards/share-dashboards-panels/shared-dashboards/index.md b/docs/sources/dashboards/share-dashboards-panels/shared-dashboards/index.md index 5518ef51146..9ea6df4e47e 100644 --- a/docs/sources/dashboards/share-dashboards-panels/shared-dashboards/index.md +++ b/docs/sources/dashboards/share-dashboards-panels/shared-dashboards/index.md @@ -156,7 +156,7 @@ On this screen, you can see: - The earliest time a user has been active in a dashboard - When they last accessed a shared dashboard -- The dashboards to they have access +- The dashboards they have access to - Their role You can also revoke a user's access to all shared dashboards on from this tab. From 8ec162afec5a16073ea0cff99a0c79062615530c Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Mon, 6 Oct 2025 09:44:13 -0400 Subject: [PATCH 015/578] chore(tracing): Initialize tracing early, before wire (#112007) Signed-off-by: Dave Henderson --- pkg/cmd/grafana-server/commands/cli.go | 9 +++++++-- pkg/infra/tracing/tracing.go | 23 ++++++++++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/grafana-server/commands/cli.go b/pkg/cmd/grafana-server/commands/cli.go index 8850132105f..f13b3bada3e 100644 --- a/pkg/cmd/grafana-server/commands/cli.go +++ b/pkg/cmd/grafana-server/commands/cli.go @@ -11,9 +11,7 @@ import ( "syscall" "time" - "github.com/grafana/grafana/pkg/services/featuremgmt" _ "github.com/grafana/pyroscope-go/godeltaprof/http/pprof" - "github.com/urfave/cli/v2" "github.com/grafana/grafana/pkg/api" @@ -21,8 +19,10 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/process" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/server" "github.com/grafana/grafana/pkg/services/apiserver/standalone" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" ) @@ -111,6 +111,11 @@ func RunServer(opts standalone.BuildInfo, cli *cli.Context) error { return err } + // Initialize tracing early to ensure it's always available for other services + if err := tracing.InitTracing(cfg); err != nil { + return err + } + s, err := server.Initialize( cli.Context, cfg, diff --git a/pkg/infra/tracing/tracing.go b/pkg/infra/tracing/tracing.go index f8b340d12df..c015569cf8a 100644 --- a/pkg/infra/tracing/tracing.go +++ b/pkg/infra/tracing/tracing.go @@ -11,6 +11,8 @@ import ( "sync" "time" + "github.com/go-kit/log/level" + "github.com/grafana/dskit/services" jaegerpropagator "go.opentelemetry.io/contrib/propagators/jaeger" "go.opentelemetry.io/contrib/samplers/jaegerremote" "go.opentelemetry.io/otel" @@ -27,11 +29,9 @@ import ( "go.opentelemetry.io/otel/trace/noop" "google.golang.org/grpc/credentials" - "github.com/go-kit/log/level" - - "github.com/grafana/dskit/services" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/setting" ) const ( @@ -105,6 +105,23 @@ func ProvideService(tracingCfg *TracingConfig) (*TracingService, error) { return ots, nil } +// InitTracing initializes the tracing service with the provided configuration. +// Used to initialize tracing early to ensure it's always available for other +// services, outside of the wire context. +func InitTracing(cfg *setting.Cfg) error { + tracingCfg, err := ParseTracingConfig(cfg) + if err != nil { + return fmt.Errorf("parse tracing config: %w", err) + } + + _, err = ProvideService(tracingCfg) + if err != nil { + return fmt.Errorf("initialize tracing: %w", err) + } + + return nil +} + func NewNoopTracerService() *TracingService { tp := &noopTracerProvider{TracerProvider: noop.NewTracerProvider()} otel.SetTracerProvider(tp) From 98f293e229108e2d72d4116bcfb66ab801ba6981 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Mon, 6 Oct 2025 15:22:00 +0100 Subject: [PATCH 016/578] OpenFeature: Create basic frontend client + example usage (#110587) * initial basic OpenFeature client for datasource class * add dep * update, use a wrapping function to enforce types * move init OF to grafana-runtime * docs * Fix circular dependency causing tests to fail * codeowners * use toggle in datasourcewithbackend * Fix CUJs group-by test * Comments * update docs, make default value mandatory * revert using for queryServiceFromUI toggle --- .github/CODEOWNERS | 1 + contribute/feature-toggles.md | 25 +++++++++++- .../dashboard-cujs/group-by-cujs.spec.ts | 5 ++- package.json | 1 + packages/grafana-runtime/package.json | 3 ++ .../grafana-runtime/src/internal/index.ts | 2 + .../src/internal/openFeature/index.ts | 33 +++++++++++++++ public/app/app.ts | 13 ++++++ yarn.lock | 40 +++++++++++++++++++ 9 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 packages/grafana-runtime/src/internal/openFeature/index.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bf54aeed6b4..22ed0a32a5e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -629,6 +629,7 @@ /packages/grafana-runtime/rollup.config.ts @grafana/grafana-frontend-platform /packages/grafana-runtime/src/index.ts @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend /packages/grafana-runtime/src/internal/index.ts @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend +/packages/grafana-runtime/src/internal/openFeature @grafana/grafana-frontend-platform /packages/grafana-runtime/src/unstable.ts @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend /packages/grafana-runtime/tsconfig.build.json @grafana/grafana-frontend-platform /packages/grafana-runtime/tsconfig.json @grafana/grafana-frontend-platform diff --git a/contribute/feature-toggles.md b/contribute/feature-toggles.md index 8e81c9c0c11..1e6b1b1b885 100644 --- a/contribute/feature-toggles.md +++ b/contribute/feature-toggles.md @@ -14,7 +14,30 @@ Once your feature toggle is defined, you can then wrap your feature around a che Examples: - [Backend](https://github.com/grafana/grafana/blob/feb2b5878b3e3ec551d64872c35edec2a0187812/pkg/services/authn/clients/session.go#L57): Use the `IsEnabled` function and pass in your feature toggle. -- [Frontend](https://github.com/grafana/grafana/blob/feb2b5878b3e3ec551d64872c35edec2a0187812/public/app/features/search/service/folders.ts#L14): Check the config for your feature toggle. + +### Frontend + +Use the new OpenFeature-based feature flag client for all new feature flags. There are some differences compared to the legacy `config.featureToggles` system: + +- Feature flag initialisation is async, but will be finished by the time the UI is rendered. This means you cannot get the value of a feature flag at the 'top level' of a module/file +- Call `evaluateBooleanFlag("flagName")` from `@grafana/runtime/internal` instead to get the value of a feature flag +- Feature flag values _may_ change over the lifetime of the session. Do not store the value in a variable that is used for longer than a single render - always call `evaluateBooleanFlag` lazily when you use the value. + +e.g. + +```ts +import { evaluateBooleanFlag } from '@grafana/runtime/internal'; + +// BAD - Don't do this. The feature toggle will not evaluate correctly +const isEnabled = evaluateBooleanFlag('newPreferences', false); + +function makeAPICall() { + // GOOD - The feature toggle should be called after app initialisation + if (evaluateBooleanFlag('newPreferences', false)) { + // do new things + } +} +``` ## Enabling toggles in development diff --git a/e2e-playwright/dashboard-cujs/group-by-cujs.spec.ts b/e2e-playwright/dashboard-cujs/group-by-cujs.spec.ts index 65e37aefbed..ad5008a0be1 100644 --- a/e2e-playwright/dashboard-cujs/group-by-cujs.spec.ts +++ b/e2e-playwright/dashboard-cujs/group-by-cujs.spec.ts @@ -71,9 +71,12 @@ test.describe( await test.step('3.Edit and restore default groupBy', async () => { const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST }); + // Wait for the page to load + const groupByVariable = getGroupByInput(dashboardPage, selectors); + await expect(groupByVariable).toBeVisible(); + const initialSelectedOptionsCount = await groupByValues.count(); - const groupByVariable = getGroupByInput(dashboardPage, selectors); await groupByVariable.click(); const groupByOption = groupByOptions.nth(1); diff --git a/package.json b/package.json index 3bece98c2ab..64006c8d73c 100644 --- a/package.json +++ b/package.json @@ -302,6 +302,7 @@ "@locker/near-membrane-shared-dom": "0.14.0", "@msagl/core": "^1.1.19", "@msagl/parser": "^1.1.19", + "@openfeature/web-sdk": "^1.6.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/exporter-collector": "0.25.0", "@opentelemetry/semantic-conventions": "1.37.0", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index c4e32ce6340..f9ba7ce203d 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -58,6 +58,9 @@ "@grafana/faro-web-sdk": "^1.13.2", "@grafana/schema": "12.3.0-pre", "@grafana/ui": "12.3.0-pre", + "@openfeature/core": "^1.9.0", + "@openfeature/ofrep-web-provider": "^0.3.3", + "@openfeature/web-sdk": "^1.6.1", "@types/systemjs": "6.15.3", "history": "4.10.1", "lodash": "4.17.21", diff --git a/packages/grafana-runtime/src/internal/index.ts b/packages/grafana-runtime/src/internal/index.ts index de669f7af74..aed6b86ebfb 100644 --- a/packages/grafana-runtime/src/internal/index.ts +++ b/packages/grafana-runtime/src/internal/index.ts @@ -27,3 +27,5 @@ export { } from '../services/pluginExtensions/getObservablePluginLinks'; export { UserStorage } from '../utils/userStorage'; + +export { initOpenFeature, evaluateBooleanFlag } from './openFeature'; diff --git a/packages/grafana-runtime/src/internal/openFeature/index.ts b/packages/grafana-runtime/src/internal/openFeature/index.ts new file mode 100644 index 00000000000..891eefe7958 --- /dev/null +++ b/packages/grafana-runtime/src/internal/openFeature/index.ts @@ -0,0 +1,33 @@ +import { OFREPWebProvider } from '@openfeature/ofrep-web-provider'; +import { OpenFeature } from '@openfeature/web-sdk'; + +import { FeatureToggles } from '@grafana/data'; + +import { config } from '../../config'; + +export type FeatureFlagName = keyof FeatureToggles; + +export async function initOpenFeature() { + /** + * Note: Currently we don't have a way to override OpenFeature flags for tests or localStorage. + * A few improvements we could make: + * - When running in tests (unit or e2e?), we could use InMemoryProvider instead + * - Use Multi-Provider to combine InMemoryProvider (for localStorage) with OFREPWebProvider + * to allow for overrides https://github.com/open-feature/js-sdk-contrib/tree/main/libs/providers/multi-provider + */ + + const ofProvider = new OFREPWebProvider({ + baseUrl: '/apis/features.grafana.app/v0alpha1/namespaces/' + config.namespace, + pollInterval: -1, // disable polling + timeoutMs: 5_000, + }); + + await OpenFeature.setProviderAndWait(ofProvider, { + targetingKey: config.namespace, + namespace: config.namespace, + }); +} + +export function evaluateBooleanFlag(flagName: FeatureFlagName, defaultValue: boolean): boolean { + return OpenFeature.getClient().getBooleanValue(flagName, defaultValue); +} diff --git a/public/app/app.ts b/public/app/app.ts index 25c730a2d34..7896117c1e7 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -43,6 +43,7 @@ import { setMegaMenuOpenHook, } from '@grafana/runtime'; import { + initOpenFeature, setGetObservablePluginComponents, setGetObservablePluginLinks, setPanelDataErrorView, @@ -129,8 +130,20 @@ export class GrafanaApp { async init() { try { await preInitTasks(); + // Let iframe container know grafana has started loading window.parent.postMessage('GrafanaAppInit', '*'); + + // Currently the OpenFeature API requires a signed in user. This means feature flags cannot be used + // on the login page. + if (contextSrv.user.isSignedIn) { + try { + await initOpenFeature(); + } catch (err) { + console.error('Failed to initialize OpenFeature provider', err); + } + } + const regionalFormat = config.featureToggles.localeFormatPreference ? config.regionalFormat : contextSrv.user.language; diff --git a/yarn.lock b/yarn.lock index 3f6986db4f0..00317070cb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3489,6 +3489,9 @@ __metadata: "@grafana/faro-web-sdk": "npm:^1.13.2" "@grafana/schema": "npm:12.3.0-pre" "@grafana/ui": "npm:12.3.0-pre" + "@openfeature/core": "npm:^1.9.0" + "@openfeature/ofrep-web-provider": "npm:^0.3.3" + "@openfeature/web-sdk": "npm:^1.6.1" "@rollup/plugin-node-resolve": "npm:16.0.1" "@rollup/plugin-terser": "npm:0.4.4" "@testing-library/dom": "npm:10.4.1" @@ -5752,6 +5755,42 @@ __metadata: languageName: node linkType: hard +"@openfeature/core@npm:^1.9.0": + version: 1.9.0 + resolution: "@openfeature/core@npm:1.9.0" + checksum: 10/c6d20edc09053afd99752fe46d8328158680950bca4b86679f67f79249d7226eea127b31fffdc38e26ecb729f2bab5a4a5a7c1db708ae76b7fbbac68cd56f094 + languageName: node + linkType: hard + +"@openfeature/ofrep-core@npm:^1.0.0": + version: 1.1.0 + resolution: "@openfeature/ofrep-core@npm:1.1.0" + peerDependencies: + "@openfeature/core": ^1.6.0 + checksum: 10/4198f2f1abf974822bf14530a7f514292d8235552d6e61465d29ffe42d092f675e7f56a9e9da5aa7d45dfbd98cc36316efbdaadaf83e8f510f35865612f5f24f + languageName: node + linkType: hard + +"@openfeature/ofrep-web-provider@npm:^0.3.3": + version: 0.3.3 + resolution: "@openfeature/ofrep-web-provider@npm:0.3.3" + dependencies: + "@openfeature/ofrep-core": "npm:^1.0.0" + peerDependencies: + "@openfeature/web-sdk": ^1.4.0 + checksum: 10/85f362e3ebaa9d421be91e4d966284e28850649e417d5db81181960b95ac693c9faa4a0bdc84eeb03fa694a8cd1e1f5d9de9bf0fba62f8e2669f9637836f0884 + languageName: node + linkType: hard + +"@openfeature/web-sdk@npm:^1.6.1": + version: 1.6.1 + resolution: "@openfeature/web-sdk@npm:1.6.1" + peerDependencies: + "@openfeature/core": ^1.9.0 + checksum: 10/8bd7d1ea386e21cdd7492cab2fd1d2b138b4e6a376a4c0a40244633e5955f6452039bc2633fc5230bd7b494506a4137ba7210d40850634f9618f77a0ee435f9d + languageName: node + linkType: hard + "@opentelemetry/api-logs@npm:0.202.0": version: 0.202.0 resolution: "@opentelemetry/api-logs@npm:0.202.0" @@ -18218,6 +18257,7 @@ __metadata: "@msagl/core": "npm:^1.1.19" "@msagl/parser": "npm:^1.1.19" "@npmcli/package-json": "npm:^6.0.0" + "@openfeature/web-sdk": "npm:^1.6.1" "@opentelemetry/api": "npm:1.9.0" "@opentelemetry/exporter-collector": "npm:0.25.0" "@opentelemetry/semantic-conventions": "npm:1.37.0" From 510b86450c87aa46913cc05faff7358d0a48237c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Bedi?= Date: Mon, 6 Oct 2025 16:57:32 +0200 Subject: [PATCH 017/578] PostgreSQL: Refactor feature toggle handling for standalone mode (#112061) --- pkg/tsdb/grafana-postgresql-datasource/postgres.go | 11 ++++------- .../grafana-postgresql-datasource/postgres_service.go | 3 ++- .../grafana-postgresql-datasource/standalone/main.go | 7 ++----- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres.go b/pkg/tsdb/grafana-postgresql-datasource/postgres.go index ca4b4eb5565..0a56b17f0f7 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres.go @@ -15,7 +15,6 @@ import ( "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/grafana/grafana/pkg/services/featuremgmt" "github.com/jackc/pgx/v5/pgxpool" "github.com/lib/pq" @@ -129,7 +128,7 @@ func newPostgresPGX(ctx context.Context, userFacingDefaultError string, rowLimit return p, handler, nil } -func NewInstanceSettings(logger log.Logger, features featuremgmt.FeatureToggles, dataPath string) datasource.InstanceFactoryFunc { +func NewInstanceSettings(logger log.Logger, usePGX bool, dataPath string) datasource.InstanceFactoryFunc { return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { cfg := backend.GrafanaConfigFromContext(ctx) sqlCfg, err := cfg.SQL() @@ -167,14 +166,12 @@ func NewInstanceSettings(logger log.Logger, features featuremgmt.FeatureToggles, DecryptedSecureJSONData: settings.DecryptedSecureJSONData, } - isPGX := features.IsEnabled(ctx, featuremgmt.FlagPostgresDSUsePGX) - userFacingDefaultError, err := cfg.UserFacingDefaultError() if err != nil { return nil, err } - if isPGX { + if usePGX { pgxlogger := logger.FromContext(ctx).With("driver", "pgx") pgxTlsManager := newPgxTlsManager(pgxlogger) pgxTlsSettings, err := pgxTlsManager.getTLSSettings(dsInfo) @@ -184,7 +181,7 @@ func NewInstanceSettings(logger log.Logger, features featuremgmt.FeatureToggles, // Ensure cleanupCertFiles is called after the connection is opened defer pgxTlsManager.cleanupCertFiles(pgxTlsSettings) - cnnstr, err := generateConnectionString(dsInfo, pgxTlsSettings, isPGX, pgxlogger) + cnnstr, err := generateConnectionString(dsInfo, pgxTlsSettings, usePGX, pgxlogger) if err != nil { return "", err } @@ -202,7 +199,7 @@ func NewInstanceSettings(logger log.Logger, features featuremgmt.FeatureToggles, if err != nil { return "", err } - cnnstr, err := generateConnectionString(dsInfo, tlsSettings, isPGX, pqlogger) + cnnstr, err := generateConnectionString(dsInfo, tlsSettings, usePGX, pqlogger) if err != nil { return nil, err } diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_service.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_service.go index 5a1050ba3a5..06418e92f78 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres_service.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres_service.go @@ -20,8 +20,9 @@ type Service struct { func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles) *Service { logger := backend.NewLoggerWith("logger", "tsdb.postgres") + usePGX := features.IsEnabled(context.Background(), featuremgmt.FlagPostgresDSUsePGX) s := &Service{ - im: datasource.NewInstanceManager(NewInstanceSettings(logger, features, cfg.DataPath)), + im: datasource.NewInstanceManager(NewInstanceSettings(logger, usePGX, cfg.DataPath)), features: features, } return s diff --git a/pkg/tsdb/grafana-postgresql-datasource/standalone/main.go b/pkg/tsdb/grafana-postgresql-datasource/standalone/main.go index c13adbb7825..9c821941e71 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/standalone/main.go +++ b/pkg/tsdb/grafana-postgresql-datasource/standalone/main.go @@ -6,7 +6,6 @@ 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/log" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" postgres "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource" ) @@ -14,11 +13,9 @@ import ( func main() { // No need to pass logger name, it will be set by the plugin SDK logger := backend.NewLoggerWith() - // TODO: get rid of setting.NewCfg() and featuremgmt.FeatureToggles once PostgresDSUsePGX is removed + // TODO: get rid of setting.NewCfg() once PostgresDSUsePGX is removed cfg := setting.NewCfg() - // We want to enable the feature toggle for api server - features := featuremgmt.WithFeatures(featuremgmt.FlagPostgresDSUsePGX) - if err := datasource.Manage("grafana-postgresql-datasource", postgres.NewInstanceSettings(logger, features, cfg.DataPath), datasource.ManageOpts{}); err != nil { + if err := datasource.Manage("grafana-postgresql-datasource", postgres.NewInstanceSettings(logger, true, cfg.DataPath), datasource.ManageOpts{}); err != nil { log.DefaultLogger.Error(err.Error()) os.Exit(1) } From 6e47fefc63c5d3d005e740561b43ba594b3e31e7 Mon Sep 17 00:00:00 2001 From: Anna Urbiztondo Date: Mon, 6 Oct 2025 17:02:14 +0200 Subject: [PATCH 018/578] Docs: OaC/Git Sync updates for Obs Con (#111792) * Changes for Obs Con * Edits * Minor renderer tweak * Renderer, caution notes * Prettier * Public * Update docs/sources/observability-as-code/provision-resources/git-sync-setup.md Co-authored-by: Jack Baldry * Jack's feedback * Link text * Messaging * Notes * Prettier --------- Co-authored-by: Jack Baldry --- .../provision-resources/_index.md | 18 ++-- .../provision-resources/file-path-setup.md | 21 +++-- .../provision-resources/git-sync-setup.md | 89 ++++++++++--------- .../provision-resources/intro-git-sync.md | 69 ++++++++------ .../provisioned-dashboards.md | 25 +++--- .../provision-resources/use-git-sync.md | 22 +++-- 6 files changed, 136 insertions(+), 108 deletions(-) diff --git a/docs/sources/observability-as-code/provision-resources/_index.md b/docs/sources/observability-as-code/provision-resources/_index.md index d920f6d5009..c8e6b313eec 100644 --- a/docs/sources/observability-as-code/provision-resources/_index.md +++ b/docs/sources/observability-as-code/provision-resources/_index.md @@ -18,16 +18,18 @@ weight: 300 # Provision resources and sync dashboards {{< admonition type="caution" >}} -Provisioning is an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. This feature is not publicly available in Grafana Cloud yet. Only the cloud-hosted version of GitHub (GitHub.com) is supported at this time. GitHub Enterprise is not yet compatible. -Sign up for Grafana Cloud Git Sync early access using [this form](https://forms.gle/WKkR3EVMcbqsNnkD9). +Git Sync is available in [private preview](https://grafana.com/docs/release-life-cycle/) for Grafana Cloud. Support and documentation is available but might be limited to enablement, configuration, and some troubleshooting. No SLAs are provided. You can sign up to the private preview using the [Git Sync early access form](https://forms.gle/WKkR3EVMcbqsNnkD9). + +Git Sync and local file provisioning are [experimental features](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. + {{< /admonition >}} -Provisioning is an experimental feature that allows you to configure how to store your dashboard JSONs and other files in GitHub repositories using either Git Sync or a local path. +Provisioning allows you to configure how to store your dashboard JSON and other files in GitHub repositories using either Git Sync or a local path. -Of the two options, **Git Sync** is the favorited method for provisioning your dashboards. You can synchronize any new dashboards and changes to existing dashboards from the UI to your configured GitHub repository. If you push a change in the repository, those changes are mirrored in your Grafana instance. See [Git Sync workflow](#git-sync-workflow). +Of the two options, **Git Sync** is the favorited method for provisioning your dashboards. You can synchronize any new dashboards and changes to existing dashboards from the UI to your configured GitHub repository. If you push a change in the repository, those changes are mirrored in your Grafana instance. Refer to [Git Sync workflow](#git-sync-workflow) for more information. -Alternatively, **local file provisioning** allows you to include in your Grafana instance resources (such as folders and dashboard JSON files) that are stored in a local file system. See [Local file workflow](local-file-workflow). +Alternatively, **local file provisioning** allows you to include in your Grafana instance resources (such as folders and dashboard JSON files) that are stored in a local file system. Refer to [Local file workflow](#local-file-workflow) for more information. ## Provisioned folders and connections @@ -40,8 +42,7 @@ You can set a single folder, or multiple folders to a different repository, with In the Git Sync workflow: - When you provision resources with Git Sync you can modify them from within the Grafana UI or within the GitHub repository. Changes made in either the repository or the Grafana UI are bidirectional. -- Any changes made in the provisioned files stored in the GitHub repository are reflected in the Grafana database. By default, Grafana polls GitHub every 60 seconds. -- The Grafana UI reads from the database and updates the UI to reflect these changes. +- Any changes made in the provisioned files stored in the GitHub repository are reflected in the Grafana database. By default, Grafana polls GitHub every 60 seconds. The Grafana UI reads from the database and updates the UI to reflect these changes. For example, if you update a dashboard within the Grafana UI and click **Save** to preserve the changes, you'll be notified that the dashboard is provisioned in a GitHub repository. Next you'll be prompted to choose how to preserve the changes: either directly to a branch, or pushed to a new branch using a pull request in GitHub. @@ -52,8 +53,7 @@ For more information, see [Introduction to Git Sync](https://grafana.com/docs/gr In the local file workflow: - All provisioned resources are changed in the local files. -- Any changes made in the provisioned files are reflected in the Grafana database. -- The Grafana UI reads the database and updates the UI to reflect these changes. +- Any changes made in the provisioned files are reflected in the Grafana database. The Grafana UI reads the database and updates the UI to reflect these changes. - You can't use the Grafana UI to edit or delete provisioned resources. Learn more in [Set up file provisioning](https://grafana.com/docs/grafana//observability-as-code/provision-resources/file-path-setup/). diff --git a/docs/sources/observability-as-code/provision-resources/file-path-setup.md b/docs/sources/observability-as-code/provision-resources/file-path-setup.md index 1832e8ad001..98fd256e594 100644 --- a/docs/sources/observability-as-code/provision-resources/file-path-setup.md +++ b/docs/sources/observability-as-code/provision-resources/file-path-setup.md @@ -16,9 +16,8 @@ weight: 200 # Set up file provisioning {{< admonition type="caution" >}} -Local file provisioning is an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. Enable the `provisioning` and `kubernetesDashboards` feature toggles in Grafana to use this feature. This feature is not publicly available in Grafana Cloud yet. Only the cloud-hosted version of GitHub (GitHub.com) is supported at this time. GitHub Enterprise is not yet compatible. -Sign up for Grafana Cloud Git Sync early access using [this form](https://forms.gle/WKkR3EVMcbqsNnkD9). +Local file provisioning is an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions, but it's **not available in Grafana Cloud**. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. {{< /admonition >}} @@ -48,10 +47,14 @@ Refer to [Provision Grafana](https://grafana.com/docs/grafana// ### Limitations - A provisioned dashboard can't be deleted from within Grafana UI. The dashboard has to be deleted at the local file system and those changes synced to Grafana. -- Changes from the local file system are one way: you can't save changes from the UI to GitHub. +- Changes from the local file system are one way: you can't save changes from the Grafana UI to GitHub. ## Before you begin +{{< admonition type="note" >}} +Enable the `provisioning` and `kubernetesDashboards` feature toggles in Grafana to use this feature. +{{< /admonition >}} + To set up file provisioning, you need: - Administration rights in your Grafana organization. @@ -122,15 +125,15 @@ The set up process verifies the path and provides an error message if a problem ### Choose what to synchronize -In this section, you determine the actions taken with the storage you selected. +Choose to either sync your entire organization resources with external storage, or to sync certain resources to a new Grafana folder (with up to 10 connections). -1. Select how resources should be handled in Grafana. +- Choose **Sync all resources with external storage** if you want to sync and manage your entire Grafana instance through external storage. With this option, all of your dashboards are synced to that one repository. You can only have one provisioned connection with this selection, and you won't have the option of setting up additional repositories to connect to. -- Choose **Sync all resources with external storage** if you want to sync and manage your entire Grafana instance through external storage. You can only have one provisioned connection with this selection. -- Choose **Sync external storage to new Grafana folder** to sync external resources into a new folder without affecting the rest of your instance. You can repeat this process for up to 10 folders. - Enter a **Display name** for the repository connection. Resources stored in this connection appear under the chosen display name in the Grafana UI. - +- Choose **Sync external storage to new Grafana folder** to sync external resources into a new folder without affecting the rest of your instance. You can repeat this process for up to 10 connections. -1. Select **Synchronize** to continue. +Next, enter a **Display name** for the repository connection. Resources stored in this connection appear under the chosen display name in the Grafana UI. + +Click **Synchronize** to continue. ### Synchronize with external storage diff --git a/docs/sources/observability-as-code/provision-resources/git-sync-setup.md b/docs/sources/observability-as-code/provision-resources/git-sync-setup.md index b0c44727ffd..fbe43aa7a57 100644 --- a/docs/sources/observability-as-code/provision-resources/git-sync-setup.md +++ b/docs/sources/observability-as-code/provision-resources/git-sync-setup.md @@ -16,50 +16,60 @@ weight: 100 # Set up Git Sync {{< admonition type="caution" >}} -Git Sync is an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. Enable the `provisioning` and `kubernetesDashboards` feature toggles in Grafana to use this feature. This feature is not publicly available in Grafana Cloud yet. Only the cloud-hosted version of GitHub (GitHub.com) is supported at this time. GitHub Enterprise is not yet compatible. -Sign up for Grafana Cloud Git Sync early access using [this form](https://forms.gle/WKkR3EVMcbqsNnkD9). +Git Sync is available in [private preview](https://grafana.com/docs/release-life-cycle/) for Grafana Cloud, and is an [experimental feature](https://grafana.com/docs/release-life-cycle/) in Grafana v12 for open source and Enterprise editions. + +Support and documentation is available but might be limited to enablement, configuration, and some troubleshooting. No SLAs are provided. + +You can sign up to the private preview using the [Git Sync early access form](https://forms.gle/WKkR3EVMcbqsNnkD9). {{< /admonition >}} -Git Sync lets you manage Grafana dashboards as code by storing dashboards JSON files and folders in a remote GitHub repository. -Alternatively, you can configure a local file system instead of using GitHub. -Refer to [Set up file provisioning](https://grafana.com/docs/grafana//observability-as-code/provision-resources/file-path-setup/) for information. +Git Sync lets you manage Grafana dashboards as code by storing dashboard JSON files and folders in a remote GitHub repository. -This page explains how to use Git Sync with a GitHub repository. +To set up Git Sync and synchronize with a GitHub repository follow these steps: -To set up Git Sync, you need to: +1. [Enable feature toggles in Grafana](#enable-required-feature-toggles) (first time set up). +1. [Create a GitHub access token](#create-a-github-access-token). +1. [Configure a connection to your GitHub repository](#set-up-the-connection-to-github). +1. [Choose what content to sync with Grafana](#choose-what-to-synchronize). -1. Enable feature toggles in Grafana (first time set up). -1. Configure a connection to your GitHub repository. -1. Choose what content to sync with Grafana. -1. Optional: Extend Git Sync by enabling pull request notifications and image previews of dashboard changes. +Optionally, you can [extend Git Sync](#configure-webhooks-and-image-rendering) by enabling pull request notifications and image previews of dashboard changes. -| Capability | Benefit | Requires | -| ----------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------- | -| Adds a table summarizing changes to your pull request | Provides a convenient way to save changes back to GitHub. | Webhooks configured | -| Add a dashboard preview image to a PR | View a snapshot of dashboard changes to a pull request without opening Grafana. | Image renderer plugin and webhooks configured | +| Capability | Benefit | Requires | +| ----------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------- | +| Adds a table summarizing changes to your pull request | Provides a convenient way to save changes back to GitHub. | Webhooks configured | +| Add a dashboard preview image to a PR | View a snapshot of dashboard changes to a pull request without opening Grafana. | Image renderer and webhooks configured | + +{{< admonition type="note" >}} + +Alternatively, you can configure a local file system instead of using GitHub. Refer to [Set up file provisioning](https://grafana.com/docs/grafana//observability-as-code/provision-resources/file-path-setup/) for more information. + +{{< /admonition >}} ## Performance impacts of enabling Git Sync -Git Sync is an experimental feature and is under continuous development. +Git Sync is an experimental feature and is under continuous development. Reporting any issues you encounter can help us improve Git Sync. -We recommend evaluating the performance impact, if any, in a non-production environment. - -When Git Sync is enabled, the database load might increase, especially for instances with a lot of folders and nested folders. -Reporting any issues you encounter can help us improve Git Sync. +When Git Sync is enabled, the database load might increase, especially for instances with a lot of folders and nested folders. Evaluate the performance impact, if any, in a non-production environment. ## Before you begin +{{< admonition type="caution" >}} + +Refer to [Known limitations](https://grafana.com/docs/grafana//observability-as-code/provision-resources/intro-git-sync#known-limitations/) before using Git Sync. + +{{< /admonition >}} + To set up Git Sync, you need: - Administration rights in your Grafana organization. - Enable the required feature toggles in your Grafana instance. Refer to [Enable required feature toggles](#enable-required-feature-toggles) for instructions. - A GitHub repository to store your dashboards in. - If you want to use a local file path, refer to [the local file path guide](https://grafana.com/docs/grafana//observability-as-code/provision-resources/file-path-setup/). -- A GitHub access token. The Grafana UI will also explain this to you as you set it up. +- A GitHub access token. The Grafana UI will prompt you during setup. - Optional: A public Grafana instance. -- Optional: Image Renderer plugin to save image previews with your PRs. +- Optional: The [Image Renderer service](https://github.com/grafana/grafana-image-renderer) to save image previews with your PRs. ## Enable required feature toggles @@ -118,28 +128,25 @@ To connect your GitHub repository, follow these steps: ### Choose what to synchronize -You can choose to either use one repository for an entire organization or to a new Grafana folder (up to 10 connections). -If you choose to sync all resources with external storage, then all of your dashboards are synced to that one repository. -You won't have the option of setting up additional repositories to connect to. +{{< admonition type="caution" >}} -You can choose to synchronize all resources with GitHub or you can sync resources to a new Grafana folder. -The options you have depend on the status of your GitHub repository. -For example, if you are syncing with a new or empty repository, you won't have an option to migrate dashboards. +If you're using Git Sync in Grafana Cloud you can only sync specific folders for the moment. Git Sync will be available for your full instance soon. -1. Select how resources should be handled in Grafana. +{{< /admonition >}} -- Choose **Sync all resources with external storage** if you want to sync and manage your entire Grafana instance through external storage. You can only have one provisioned connection with this selection. -- Choose **Sync external storage to new Grafana folder** to sync external resources into a new folder without affecting the rest of your instance. You can repeat this process for up to 10 connections. - Enter a **Display name** for the repository connection. Resources stored in this connection appear under the chosen display name in the Grafana UI. - +In this step you can decide which elements to synchronize. Keep in mind the available options depend on the status of your GitHub repository. The first time you connect Grafana with a GitHub repository, you need to synchronize with external storage. If you are syncing with a new or empty repository, you won't have an option to migrate dashboards. -1. Select **Synchronize** to continue. +1. Choose to either sync your entire organization resources with external storage, or to sync certain resources to a new Grafana folder (with up to 10 connections). + +- Choose **Sync all resources with external storage** if you want to sync and manage your entire Grafana instance through external storage. With this option, all of your dashboards are synced to that one repository. You can only have one provisioned connection with this selection, and you won't have the option of setting up additional repositories to connect to. + +- Choose **Sync external storage to new Grafana folder** to sync external resources into a new folder without affecting the rest of your instance. You can repeat this process for up to 10 connections. + +1. Enter a **Display name** for the repository connection. Resources stored in this connection appear under the chosen display name in the Grafana UI. +1. Click **Synchronize** to continue. - -1. Optional: If you have the Grafana Image Renderer plugin configured, you can **Enable dashboards previews in pull requests**. If image rendering is not available, then you can't select this option. For more information, refer to [Grafana Image Renderer](https://grafana.com/grafana/plugins/grafana-image-renderer/). +1. Optional: If you have the Grafana Image Renderer plugin configured, you can **Enable dashboards previews in pull requests**. If image rendering is not available, then you can't select this option. For more information, refer to the [Image Renderer service](https://github.com/grafana/grafana-image-renderer). 1. Select **Finish** to proceed. ## Verify your dashboards in Grafana To verify that your dashboards are available at the location that you specified, click **Dashboards**. The name of the dashboard is listed in the **Name** column. -Now that your dashboards have been synced from a repository, you can customize the name, change the branch, and create a pull request (PR) for it. -Refer to [Use Git Sync](https://grafana.com/docs/grafana//observability-as-code/provision-resources/use-git-sync/) for more information. +Now that your dashboards have been synced from a repository, you can customize the name, change the branch, and create a pull request (PR) for it. Refer to [Manage provisioned repositories with Git Sync](https://grafana.com/docs/grafana//observability-as-code/provision-resources/use-git-sync/) for more information. ## Configure webhooks and image rendering @@ -214,8 +220,7 @@ The necessary paths required to be exposed are (RegExp): By setting up image rendering, you can add visual previews of dashboard updates directly in pull requests. Image rendering also requires webhooks. -You can enable this capability by installing the Grafana Image Renderer plugin in your Grafana instance. -For more information and installation instructions, refer to [Grafana Image Renderer](https://grafana.com/grafana/plugins/grafana-image-renderer/). +You can enable this capability by installing the Grafana Image Renderer in your Grafana instance. For more information and installation instructions, refer to the [Image Renderer service](https://github.com/grafana/grafana-image-renderer). ## Modify configurations after set up is complete diff --git a/docs/sources/observability-as-code/provision-resources/intro-git-sync.md b/docs/sources/observability-as-code/provision-resources/intro-git-sync.md index 42bc9691264..2c80d39180b 100644 --- a/docs/sources/observability-as-code/provision-resources/intro-git-sync.md +++ b/docs/sources/observability-as-code/provision-resources/intro-git-sync.md @@ -16,9 +16,12 @@ weight: 100 # Introduction to Git Sync {{< admonition type="caution" >}} -Git Sync is an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. Enable the `provisioning` and `kubernetesDashboards` feature toggles in Grafana to use this feature. This feature is not publicly available in Grafana Cloud yet. Only the cloud-hosted version of GitHub (GitHub.com) is supported at this time. GitHub Enterprise is not yet compatible. -Sign up for Grafana Cloud Git Sync early access using [this form](https://forms.gle/WKkR3EVMcbqsNnkD9). +Git Sync is available in [private preview](https://grafana.com/docs/release-life-cycle/) for Grafana Cloud, and is an [experimental feature](https://grafana.com/docs/release-life-cycle/) in Grafana v12 for open source and Enterprise editions. + +Support and documentation is available but might be limited to enablement, configuration, and some troubleshooting. No SLAs are provided. + +You can sign up to the private preview using the [Git Sync early access form](https://forms.gle/WKkR3EVMcbqsNnkD9). {{< /admonition >}} @@ -28,55 +31,65 @@ Using Git Sync, you can: - Manage dashboard configuration outside of Grafana instances - Replicate dashboards across multiple instances -Whenever a dashboard is modified, Grafana can commit changes to Git upon saving. Users can configure settings to either enforce PR approvals before merging or allow direct commits. - -Users can push changes directly to GitHub and see them in Grafana. Similarly, automated workflows can do changes that will be automatically represented in Grafana by updating Git. - -Because the dashboards are defined in JSON files, you can enable as-code workflows where the JSON is output from Go, TypeScript, or another coding language in the format of a dashboard schema. - -To learn more about creating dashboards in a coding language to provision them for Git Sync, refer to the [Foundation SDK](https://grafana.com/docs/grafana//observability-as-code/foundation-sdk) documentation. - ## How it works -Git Sync is bidirectional and also works with changes done directly in GitHub as well as within the Grafana UI. -Grafana periodically polls GitHub at a regular internal to synchronize any changes. -With the webhooks feature enabled, repository notifications appear almost immediately. -Without webhooks, Grafana polls for changes at the specified interval. -The default polling interval is 60 seconds. +Because dashboards are defined in JSON files, you can enable as-code workflows where the JSON file is an output from Go, TypeScript, or another coding language in the format of a dashboard schema. To learn more about creating dashboards in a coding language to provision them for Git Sync, refer to the [Foundation SDK](https://grafana.com/docs/grafana//observability-as-code/foundation-sdk) documentation. -Any changes made in the provisioned files stored in the GitHub repository are reflected in the Grafana database. -The Grafana UI reads the database and updates the UI to reflect these changes. +Git Sync is bidirectional and works both with changes done directly in GitHub as well as in the Grafana UI. + +### Make changes in Grafana + +Whenever you modify a dashboard directly from the UI, Grafana can commit changes to Git upon saving. You can configure settings to either enforce PR approvals before merging in your repository, or allow direct commits. + +Grafana periodically polls GitHub at a regular internal to synchronize any changes. The default polling interval is 60 seconds. + +- If you enable the [webhooks feature](https://grafana.com/docs/grafana//observability-as-code/provision-resources/git-sync-setup/#configure-webhooks-and-image-rendering), repository notifications appear almost immediately. +- Without webhooks, Grafana polls for changes at the specified interval. + +### Make changes in your GitHub repositories + +With Git Sync, you can make changes in your provisioned files in GitHub and see them in Grafana. Automated workflows ensure those changes are automatically represented in the Grafana database by updating Git. The Grafana UI reads the database and updates the UI to reflect these changes. + +## Known limitations + +Git Sync is under development and the following limitations apply: + +- You can only authenticate using your GitHub token. +- Support for native Git and other providers, such as GitLab or Bitbucket, is scheduled. +- If you're using Git Sync in Grafana Cloud you can only sync specific folders for the moment. Git Sync will be available for your full instance soon. +- Restoring resources from the UI is currently not possible. As an alternative, you can restore dashboards directly in your GitHub repository by raising a PR, and they will be updated in Grafana. ## Common use cases -Git Sync in Grafana lets you manage dashboards as code. -Because your dashboard JSON files are stored in GitHub, you and your team can version control, collaborate, and automate deployments efficiently. +Git Sync in Grafana lets you manage your dashboards as code as JSON files stored in GitHub. You and your team can version control, collaborate, and automate deployments efficiently. ### Version control and auditing -Organizations can maintain a structured, version-controlled history of Grafana dashboards. -The version control lets you revert to previous versions when necessary, compare modifications across commits, and ensure transparency in dashboard management. +Organizations can maintain a structured, version-controlled history of Grafana dashboards. The version control lets you revert to previous versions when necessary, compare modifications across commits, and ensure transparency in dashboard management. + Additionally, having a detailed history of changes enhances compliance efforts, as teams can generate audit logs that document who made changes, when they were made, and why. ### Automated deployment and CI/CD integration -Teams can streamline their workflow by integrating dashboard updates into their CI/CD pipelines. -By pushing changes to GitHub, automated processes can trigger validation checks, test dashboard configurations, and deploy updates programmatically using the `grafanactl` CLI and Foundation SDK. +Teams can streamline their workflow by integrating dashboard updates into their CI/CD pipelines. By pushing changes to GitHub, automated processes can trigger validation checks, test dashboard configurations, and deploy updates programmatically using the `grafanactl` CLI and Foundation SDK. + This reduces the risk of human errors, ensures consistency across environments, and enables a faster, more reliable release cycle for dashboards used in production monitoring and analytics. ### Collaborative dashboard development With Git Sync, multiple users can work on dashboards simultaneously without overwriting each other’s modifications. -By leveraging pull requests and branch-based workflows, teams can submit changes for review before merging them into the main branch. This process not only improves quality control but also ensures that dashboards adhere to best practices and organizational standards. Additionally, GitHub’s built-in discussion and review tools facilitate effective collaboration, making it easier to address feedback before changes go live. +By leveraging pull requests and branch-based workflows, teams can submit changes for review before merging them into the main branch. This process not only improves quality control but also ensures that dashboards adhere to best practices and organizational standards. + +Additionally, GitHub’s built-in discussion and review tools facilitate effective collaboration, making it easier to address feedback before changes go live. ### Multi-environment synchronization -Enterprises managing multiple Grafana instances, such as development, staging, and production environments, can seamlessly sync dashboards across these instances. -This ensures consistency in visualization and monitoring configurations, reducing discrepancies that might arise from manually managing dashboards in different environments. +Enterprises managing multiple Grafana instances, such as development, staging, and production environments, can seamlessly sync dashboards across these instances. This ensures consistency in visualization and monitoring configurations, reducing discrepancies that might arise from manually managing dashboards in different environments. + By using Git Sync, teams can automate deployments across environments, eliminating repetitive setup tasks and maintaining a standardized monitoring infrastructure across the organization. ### Disaster recovery and backup By continuously syncing dashboards to GitHub, organizations can create an always-updated backup, ensuring dashboards are never lost due to accidental deletion or system failures. -If an issue arises--such as a corrupted dashboard, unintended modification, or a system crash--teams can quickly restore the latest functional version from the Git repository. -This not only minimizes downtime but also adds a layer of resilience to Grafana monitoring setups, ensuring critical dashboards remain available when needed. + +If an issue arises, such as a corrupted dashboard, unintended modification, or a system crash, teams can quickly restore the latest functional version from the Git repository. This not only minimizes downtime but also adds a layer of resilience to Grafana monitoring setups, ensuring critical dashboards remain available when needed. diff --git a/docs/sources/observability-as-code/provision-resources/provisioned-dashboards.md b/docs/sources/observability-as-code/provision-resources/provisioned-dashboards.md index e92bb0800dc..fbe08fd3389 100644 --- a/docs/sources/observability-as-code/provision-resources/provisioned-dashboards.md +++ b/docs/sources/observability-as-code/provision-resources/provisioned-dashboards.md @@ -16,9 +16,10 @@ weight: 300 # Work with provisioned dashboards {{< admonition type="caution" >}} -Git Sync and File path provisioning an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. Enable the `provisioning` and `kubernetesDashboards` feature toggles in Grafana. These features aren't available publicly in Grafana Cloud yet. Only the cloud-hosted version of GitHub (GitHub.com) is supported at this time. GitHub Enterprise is not yet compatible. -Sign up for Grafana Cloud Git Sync early access using [this form](https://forms.gle/WKkR3EVMcbqsNnkD9). +Git Sync is available in [private preview](https://grafana.com/docs/release-life-cycle/) for Grafana Cloud. Support and documentation is available but might be limited to enablement, configuration, and some troubleshooting. No SLAs are provided. You can sign up to the private preview using the [Git Sync early access form](https://forms.gle/WKkR3EVMcbqsNnkD9). + +Git Sync and local file provisioning are [experimental features](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. {{< /admonition >}} @@ -30,16 +31,17 @@ For more information, refer to the [Dashboards](https://grafana.com/docs/grafana Dashboards and folders synchronized using Git Sync or a local file path are referred to as "provisioned" resources. -Of the two experimental options, Git Sync is the recommended method for provisioning your dashboards. +### Git Sync provisioning + +Of the two experimental options, **Git Sync** is the recommended method for provisioning your dashboards. You can synchronize any new dashboards and changes to existing dashboards to your configured GitHub repository. If you push a change in the repository, those changes are mirrored in your Grafana instance. -For more information on configuring Git Sync, refer to [Set up Git Sync](https://grafana.com/docs/grafana//observability-as-code/provision-resources/intro-git-sync/). + +For more information on configuring Git Sync, refer to [Introduction to Git Sync](https://grafana.com/docs/grafana//observability-as-code/provision-resources/intro-git-sync/). ### Local path provisioning -Using the local path provisioning makes files from a specified path available within Grafana. -These provisioned resources can only be modified in the local files and not within Grafana. -Any changes made in the configured local path are updated in Grafana. +Local path provisioning makes files from a specified path available within Grafana, and any changes made in the configured local path are updated in Grafana. Note that these provisioned resources can only be modified in the local files and not within Grafana. Refer to [Set up file provisioning](https://grafana.com/docs/grafana//observability-as-code/provision-resources/file-path-setup) to learn more about the version of local file provisioning in Grafana 12. @@ -114,9 +116,9 @@ Saving changes requires opening a pull request in your GitHub repository. ### Remove dashboards -You can remove a provisioned dashboard by deleting the dashboard from the repository. +You can remove a provisioned dashboard by deleting the dashboard from the repository. The Grafana UI updates when the changes from the GitHub repository sync. -Grafana updates when the changes from the GitHub repository sync. +To restore a deleted dashboard, raise a PR directly in your GitHub repository. Restoring resources from the UI is currently not possible. ### Tips @@ -128,9 +130,6 @@ Grafana updates when the changes from the GitHub repository sync. ## Manage dashboards provisioned with file provisioning To update any resources in the local path, you need to edit the files directly and then save them locally. -These changes are synchronized to Grafana. -However, you can't create, edit, or delete these resources using the Grafana UI. - -For more information, refer to [How it works](https://grafana.com/docs/grafana//observability-as-code/provision-resources/). +These changes are synchronized to Grafana. However, you can't create, edit, or delete these resources using the Grafana UI. Refer to [Set up file provisioning](https://grafana.com/docs/grafana//observability-as-code/provision-resources/file-path-setup/) for configuration instructions. diff --git a/docs/sources/observability-as-code/provision-resources/use-git-sync.md b/docs/sources/observability-as-code/provision-resources/use-git-sync.md index e8d323e578c..11751314865 100644 --- a/docs/sources/observability-as-code/provision-resources/use-git-sync.md +++ b/docs/sources/observability-as-code/provision-resources/use-git-sync.md @@ -19,20 +19,22 @@ weight: 400 # Manage provisioned repositories with Git Sync {{< admonition type="caution" >}} -Git Sync is an [experimental feature](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. Enable the `provisioning` and `kubernetesDashboards` feature toggles in Grafana to use this feature. This feature is not publicly available in Grafana Cloud yet. Only the cloud-hosted version of GitHub (GitHub.com) is supported at this time. GitHub Enterprise is not yet compatible. -Sign up for Grafana Cloud Git Sync early access using [this form](https://forms.gle/WKkR3EVMcbqsNnkD9). +Git Sync is available in [private preview](https://grafana.com/docs/release-life-cycle/) for Grafana Cloud, and is an [experimental feature](https://grafana.com/docs/release-life-cycle/) in Grafana v12 for open source and Enterprise editions. + +Support and documentation is available but might be limited to enablement, configuration, and some troubleshooting. No SLAs are provided. + +You can sign up to the private preview using the [Git Sync early access form](https://forms.gle/WKkR3EVMcbqsNnkD9). {{< /admonition >}} -After you have set up Git Sync, you can synchronize any changes in your existing dashboards with your configured GitHub repository. Similarly, if you push a change in the repository, those changes are mirrored in your Grafana instance. +After you have set up Git Sync, you can synchronize any changes you make in your existing provisioned folders in the UI with your configured GitHub repository. Similarly, if you push a change into your repository, those changes are mirrored in your Grafana instance. ## View current status of synchronization -Each repository synchronized with Git Sync has a dashboard that provides a summary of resources, health, pull status, webhook, sync jobs, resources, and files. -Use the detailed information accessed in **View** to help troubleshoot and understand the health of your repository's connection with Grafana. +When you synchronize a repository, Git Sync also creates a dashboard that provides a summary of resources, health, pull status, webhook, sync jobs, resources, and files. -To view the current status, follow these steps. +Use the **View** section in **Provisioning** to see detailed information about the current status of your sync, understand the health of your repository's connection with Grafana, and [troubleshoot](#troubleshoot-synchronization) possible issues: 1. Log in to your Grafana server with an account that has the Grafana Admin or Editor flag set. 1. Select **Administration** in the left-side menu and then **Provisioning**. @@ -44,7 +46,7 @@ To view the current status, follow these steps. Synchronizing resources from provisioned repositories into your Grafana instance pulls the resources into the selected folder. Existing dashboards with the same `uid` are overwritten. -To sync changes from your dashboards with your Git repository: +To sync changes from your Grafana dashboards with your Git repository: 1. From the left menu, select **Administration** > **Provisioning**. 1. Select **Pull** under the repository you want to sync. @@ -64,6 +66,12 @@ Refer to [Work with provisioned dashboards](../provisioned-dashboards) for infor ## Troubleshoot synchronization +{{< admonition type="caution" >}} + +Before you proceed to troubleshoot, understand the [known limitations](https://grafana.com/docs/grafana//observability-as-code/provision-resources/intro-git-sync#known-limitations/). + +{{< /admonition >}} + Monitor the **View** status page for synchronization issues and status updates. Common events include: - Sync started From a5ee2124401d82051e7f2351d2b89e5f486659e6 Mon Sep 17 00:00:00 2001 From: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> Date: Mon, 6 Oct 2025 10:04:02 -0500 Subject: [PATCH 019/578] Docs: Fixed a broken link on the Intro to Exemplars page (#112025) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixed broken link Co-authored-by: Irene Rodríguez --- docs/sources/fundamentals/exemplars/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/fundamentals/exemplars/index.md b/docs/sources/fundamentals/exemplars/index.md index 8b556d928d6..15e3bc1a50f 100644 --- a/docs/sources/fundamentals/exemplars/index.md +++ b/docs/sources/fundamentals/exemplars/index.md @@ -34,7 +34,7 @@ After you localize the latency problem to a few exemplar traces, you can combine Support for exemplars is available for the Prometheus data source only. After you enable the functionality, exemplar data is available by default. -For more information on exemplar configuration and how to enable exemplars, refer to [configuring exemplars in the Prometheus data source](../../datasources/prometheus/configure-prometheus-data-source/#exemplars). +For more information on exemplar configuration and how to enable exemplars, refer to the Exemplars section in [Prometheus configuration options](https://grafana.com/docs/grafana/latest/datasources/prometheus/configure/#configuration-options). Grafana shows exemplars alongside a metric in the Explore view and in dashboards. Each exemplar displays as a highlighted star. From 92990790bac5b61926aefd84f1f686cedafa78fb Mon Sep 17 00:00:00 2001 From: Alex Spencer <52186778+alexjonspencer1@users.noreply.github.com> Date: Mon, 6 Oct 2025 08:13:17 -0700 Subject: [PATCH 020/578] Transformations: Add error message during datasource or query error (#110969) * feat: add message for fields during datasource or query error * chore: remove unused Select import * chore: i18n * chore: fix tests * chore: empty commit to trigger build * chore: prune suppressions * chore: feedback * chore: i18n extract --- eslint-suppressions.json | 5 - .../editors/GroupByTransformerEditor.tsx | 25 +++-- .../app/features/transformers/utils.test.ts | 96 ++++++++++++++++++- public/app/features/transformers/utils.ts | 58 ++++++++++- public/locales/en-US/grafana.json | 1 + 5 files changed, 168 insertions(+), 17 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 227edc2fd71..2612b2c0400 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -3383,11 +3383,6 @@ "count": 1 } }, - "public/app/features/transformers/editors/GroupByTransformerEditor.tsx": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, "public/app/features/transformers/editors/GroupToNestedTableTransformerEditor.tsx": { "no-restricted-syntax": { "count": 1 diff --git a/public/app/features/transformers/editors/GroupByTransformerEditor.tsx b/public/app/features/transformers/editors/GroupByTransformerEditor.tsx index ebd2a55661d..b84753a1b20 100644 --- a/public/app/features/transformers/editors/GroupByTransformerEditor.tsx +++ b/public/app/features/transformers/editors/GroupByTransformerEditor.tsx @@ -4,7 +4,6 @@ import { useCallback } from 'react'; import { DataTransformerID, ReducerID, - SelectableValue, standardTransformers, TransformerRegistryItem, TransformerUIProps, @@ -13,12 +12,12 @@ import { } from '@grafana/data'; import { GroupByFieldOptions, GroupByOperationID, GroupByTransformerOptions } from '@grafana/data/internal'; import { t } from '@grafana/i18n'; -import { useTheme2, Select, StatsPicker, InlineField, Stack, Alert } from '@grafana/ui'; +import { useTheme2, StatsPicker, InlineField, Stack, Alert, Combobox, ComboboxOption } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; import darkImage from '../images/dark/groupBy.svg'; import lightImage from '../images/light/groupBy.svg'; -import { useAllFieldNamesFromDataFrames } from '../utils'; +import { DataFieldsErrorWrapper } from '../utils'; interface FieldProps { fieldName: string; @@ -26,9 +25,11 @@ interface FieldProps { onConfigChange: (config: GroupByFieldOptions) => void; } -const GroupByTransformerEditor = ({ input, options, onChange }: TransformerUIProps) => { - const fieldNames = useAllFieldNamesFromDataFrames(input, true); +interface GroupByTransformerEditorProps extends TransformerUIProps { + fieldNames: string[]; +} +export const GroupByTransformerEditorBase = ({ options, onChange, fieldNames }: GroupByTransformerEditorProps) => { const onConfigChange = useCallback( (fieldName: string) => (config: GroupByFieldOptions) => { onChange({ @@ -84,16 +85,20 @@ const GroupByTransformerEditor = ({ input, options, onChange }: TransformerUIPro ); }; +const GroupByTransformerEditor = DataFieldsErrorWrapper(GroupByTransformerEditorBase, { + withBaseFieldNames: true, +}); + const GroupByFieldConfiguration = ({ fieldName, config, onConfigChange }: FieldProps) => { const theme = useTheme2(); const styles = getStyles(theme); const onChange = useCallback( - (value: SelectableValue) => { + (option: ComboboxOption | null) => { onConfigChange({ aggregations: config?.aggregations ?? [], - operation: value?.value ?? null, + operation: option?.value ?? null, }); }, [config, onConfigChange] @@ -114,7 +119,7 @@ const GroupByFieldConfiguration = ({ fieldName, config, onConfigChange }: FieldP

  • kI3e#u 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%0A*nm2_rtq)%^S6*3;kJD=>%#$s`@72Fw5 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