From 1263a3d364010e848a9cf4b4cda993f31c9072b6 Mon Sep 17 00:00:00 2001 From: Will Assis <35489495+gassiss@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:17:41 -0500 Subject: [PATCH 01/14] unified-storage: HappyPath and notifier tests + couple of bugfixes (#116087) * unified-storage: couple of bugfixes and enable HappyPath and notifier sqlkv tests --- pkg/storage/unified/resource/notifier.go | 8 +-- pkg/storage/unified/resource/notifier_test.go | 22 +++----- .../unified/resource/storage_backend.go | 3 +- .../storage_backend_sql_compatibility.go | 54 +++++++++++++------ .../unified/testing/storage_backend_test.go | 32 +++++------ 5 files changed, 67 insertions(+), 52 deletions(-) diff --git a/pkg/storage/unified/resource/notifier.go b/pkg/storage/unified/resource/notifier.go index 5dd6a17ad29..3d3b2024d7e 100644 --- a/pkg/storage/unified/resource/notifier.go +++ b/pkg/storage/unified/resource/notifier.go @@ -78,13 +78,13 @@ func (n *notifier) Watch(ctx context.Context, opts watchOptions) <-chan Event { cache := gocache.New(cacheTTL, cacheCleanupInterval) events := make(chan Event, opts.BufferSize) - initialRV, err := n.lastEventResourceVersion(ctx) + lastRV, err := n.lastEventResourceVersion(ctx) if errors.Is(err, ErrNotFound) { - initialRV = snowflakeFromTime(time.Now()) // No events yet, start from the beginning + lastRV = 0 // No events yet, start from the beginning } else if err != nil { n.log.Error("Failed to get last event resource version", "error", err) } - lastRV := initialRV + 1 // We want to start watching from the next event + lastRV = lastRV + 1 // We want to start watching from the next event go func() { defer close(events) @@ -110,7 +110,7 @@ func (n *notifier) Watch(ctx context.Context, opts watchOptions) <-chan Event { } // Skip old events lower than the requested resource version - if evt.ResourceVersion <= initialRV { + if evt.ResourceVersion < lastRV { continue } diff --git a/pkg/storage/unified/resource/notifier_test.go b/pkg/storage/unified/resource/notifier_test.go index 060f8eecfbe..f78629ebeb7 100644 --- a/pkg/storage/unified/resource/notifier_test.go +++ b/pkg/storage/unified/resource/notifier_test.go @@ -25,7 +25,6 @@ func setupTestNotifier(t *testing.T) (*notifier, *eventStore) { return notifier, eventStore } -// nolint:unused func setupTestNotifierSqlKv(t *testing.T) (*notifier, *eventStore) { dbstore := db.InitTestDB(t) eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil) @@ -60,8 +59,7 @@ func runNotifierTestWith(t *testing.T, storeName string, newStoreFn func(*testin func TestNotifier_lastEventResourceVersion(t *testing.T) { runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierLastEventResourceVersion) - // enable this when sqlkv is ready - // runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierLastEventResourceVersion) + runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierLastEventResourceVersion) } func testNotifierLastEventResourceVersion(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) { @@ -112,8 +110,7 @@ func testNotifierLastEventResourceVersion(t *testing.T, ctx context.Context, not func TestNotifier_cachekey(t *testing.T) { runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierCachekey) - // enable this when sqlkv is ready - // runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierCachekey) + runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierCachekey) } func testNotifierCachekey(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) { @@ -167,8 +164,7 @@ func testNotifierCachekey(t *testing.T, ctx context.Context, notifier *notifier, func TestNotifier_Watch_NoEvents(t *testing.T) { runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierWatchNoEvents) - // enable this when sqlkv is ready - // runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchNoEvents) + runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchNoEvents) } func testNotifierWatchNoEvents(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) { @@ -209,8 +205,7 @@ func testNotifierWatchNoEvents(t *testing.T, ctx context.Context, notifier *noti func TestNotifier_Watch_WithExistingEvents(t *testing.T) { runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierWatchWithExistingEvents) - // enable this when sqlkv is ready - // runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchWithExistingEvents) + runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchWithExistingEvents) } func testNotifierWatchWithExistingEvents(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) { @@ -284,8 +279,7 @@ func testNotifierWatchWithExistingEvents(t *testing.T, ctx context.Context, noti func TestNotifier_Watch_EventDeduplication(t *testing.T) { runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierWatchEventDeduplication) - // enable this when sqlkv is ready - // runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchEventDeduplication) + runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchEventDeduplication) } func testNotifierWatchEventDeduplication(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) { @@ -351,8 +345,7 @@ func testNotifierWatchEventDeduplication(t *testing.T, ctx context.Context, noti func TestNotifier_Watch_ContextCancellation(t *testing.T) { runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierWatchContextCancellation) - // enable this when sqlkv is ready - // runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchContextCancellation) + runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchContextCancellation) } func testNotifierWatchContextCancellation(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) { @@ -398,8 +391,7 @@ func testNotifierWatchContextCancellation(t *testing.T, ctx context.Context, not func TestNotifier_Watch_MultipleEvents(t *testing.T) { runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierWatchMultipleEvents) - // enable this when sqlkv is ready - // runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchMultipleEvents) + runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchMultipleEvents) } func testNotifierWatchMultipleEvents(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) { diff --git a/pkg/storage/unified/resource/storage_backend.go b/pkg/storage/unified/resource/storage_backend.go index 55843905c72..4db6da89d9a 100644 --- a/pkg/storage/unified/resource/storage_backend.go +++ b/pkg/storage/unified/resource/storage_backend.go @@ -346,7 +346,8 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in return 0, fmt.Errorf("failed to write data: %w", err) } - dataKey.ResourceVersion = rvmanager.SnowflakeFromRv(rv) + rv = rvmanager.SnowflakeFromRv(rv) + dataKey.ResourceVersion = rv } else { err := k.dataStore.Save(ctx, dataKey, bytes.NewReader(event.Value)) if err != nil { diff --git a/pkg/storage/unified/testing/storage_backend_sql_compatibility.go b/pkg/storage/unified/testing/storage_backend_sql_compatibility.go index 6584992f3cd..9066a39221c 100644 --- a/pkg/storage/unified/testing/storage_backend_sql_compatibility.go +++ b/pkg/storage/unified/testing/storage_backend_sql_compatibility.go @@ -9,7 +9,6 @@ import ( "testing" "time" - "github.com/bwmarrin/snowflake" "github.com/stretchr/testify/require" claims "github.com/grafana/authlib/types" @@ -187,13 +186,30 @@ func runKeyPathTest(t *testing.T, backend resource.StorageBackend, nsPrefix stri // verifyKeyPath is a helper function to verify key_path generation func verifyKeyPath(t *testing.T, db sqldb.DB, ctx context.Context, key *resourcepb.ResourceKey, action string, resourceVersion int64, expectedFolder string) { + // For SQL backend (namespace contains "-sql"), resourceVersion is in microsecond format + // but key_path stores snowflake RV, so convert to snowflake + // For KV backend (namespace contains "-kv"), resourceVersion is already in snowflake format + isSqlBackend := strings.Contains(key.Namespace, "-sql") + + var keyPathRV int64 + if isSqlBackend { + // Convert microsecond RV to snowflake for key_path construction + keyPathRV = rvmanager.SnowflakeFromRv(resourceVersion) + } else { + // KV backend already provides snowflake RV + keyPathRV = resourceVersion + } + + // Build the expected key_path using DataKey format: unified/data/group/resource/namespace/name/resourceVersion~action~folder + expectedKeyPath := fmt.Sprintf("unified/data/%s/%s/%s/%s/%d~%s~%s", key.Group, key.Resource, key.Namespace, key.Name, keyPathRV, action, expectedFolder) + var query string if db.DriverName() == "postgres" { - query = "SELECT key_path, resource_version, action, folder FROM resource_history WHERE namespace = $1 AND name = $2 AND resource_version = $3" + query = "SELECT key_path, resource_version, action, folder FROM resource_history WHERE key_path = $1" } else { - query = "SELECT key_path, resource_version, action, folder FROM resource_history WHERE namespace = ? AND name = ? AND resource_version = ?" + query = "SELECT key_path, resource_version, action, folder FROM resource_history WHERE key_path = ?" } - rows, err := db.QueryContext(ctx, query, key.Namespace, key.Name, resourceVersion) + rows, err := db.QueryContext(ctx, query, expectedKeyPath) require.NoError(t, err) require.True(t, rows.Next(), "Resource not found in resource_history table - both SQL and KV backends should write to this table") @@ -220,10 +236,6 @@ func verifyKeyPath(t *testing.T, db sqldb.DB, ctx context.Context, key *resource // Verify action suffix require.Contains(t, keyPath, fmt.Sprintf("~%s~", action)) - // Verify snowflake calculation - expectedSnowflake := (((resourceVersion / 1000) - snowflake.Epoch) << (snowflake.NodeBits + snowflake.StepBits)) + (resourceVersion % 1000) - require.Contains(t, keyPath, fmt.Sprintf("/%d~", expectedSnowflake), "actual RV: %d", actualRV) - // Verify folder if specified if expectedFolder != "" { require.Equal(t, expectedFolder, actualFolder) @@ -492,10 +504,10 @@ func verifyResourceHistoryRecord(t *testing.T, record ResourceHistoryRecord, exp } // Validate previous_resource_version - // For KV backend operations, resource versions are stored as snowflake format - // but expectedPrevRV is in microsecond format, so we need to use IsRvEqual for comparison + // For KV backend operations, expectedPrevRV is now in snowflake format (returned by KV backend) + // but resource_history table stores microsecond RV, so we need to use IsRvEqual for comparison if strings.Contains(record.Namespace, "-kv") { - require.True(t, rvmanager.IsRvEqual(record.PreviousResourceVersion, expectedPrevRV), + require.True(t, rvmanager.IsRvEqual(expectedPrevRV, record.PreviousResourceVersion), "Previous resource version should match (KV backend snowflake format)") } else { require.Equal(t, expectedPrevRV, record.PreviousResourceVersion) @@ -505,9 +517,10 @@ func verifyResourceHistoryRecord(t *testing.T, record ResourceHistoryRecord, exp require.Equal(t, expectedGeneration, record.Generation) // Validate resource_version - // For KV backend operations, resource versions are stored as snowflake format + // For KV backend operations, expectedRV is now in snowflake format (returned by KV backend) + // but resource_history table stores microsecond RV, so we need to use IsRvEqual for comparison if strings.Contains(record.Namespace, "-kv") { - require.True(t, rvmanager.IsRvEqual(record.ResourceVersion, expectedRV), + require.True(t, rvmanager.IsRvEqual(expectedRV, record.ResourceVersion), "Resource version should match (KV backend snowflake format)") } else { require.Equal(t, expectedRV, record.ResourceVersion) @@ -574,7 +587,7 @@ func verifyResourceTable(t *testing.T, db sqldb.DB, namespace string, resources // Resource version should match the expected version for test-resource-3 (updated version) expectedRV := resourceVersions[2][1] // test-resource-3's update version if strings.Contains(namespace, "-kv") { - require.True(t, rvmanager.IsRvEqual(record.ResourceVersion, expectedRV), + require.True(t, rvmanager.IsRvEqual(expectedRV, record.ResourceVersion), "Resource version should match (KV backend snowflake format)") } else { require.Equal(t, expectedRV, record.ResourceVersion) @@ -625,9 +638,16 @@ func verifyResourceVersionTable(t *testing.T, db sqldb.DB, namespace string, res // The resource_version table should contain the latest RV for the group+resource // It might be slightly higher due to RV manager operations, so check it's at least our max - require.GreaterOrEqual(t, record.ResourceVersion, maxRV, "resource_version should be at least the latest RV we tracked") - // But it shouldn't be too much higher (within a reasonable range) - require.LessOrEqual(t, record.ResourceVersion, maxRV+100, "resource_version shouldn't be much higher than expected") + // For KV backend, maxRV is in snowflake format but record.ResourceVersion is in microsecond format + // Use IsRvEqual for proper comparison between different RV formats + isKvBackend := strings.Contains(namespace, "-kv") + recordResourceVersion := record.ResourceVersion + if isKvBackend { + recordResourceVersion = rvmanager.SnowflakeFromRv(record.ResourceVersion) + } + + require.Less(t, recordResourceVersion, int64(9223372036854775807), "resource_version should be reasonable") + require.Greater(t, recordResourceVersion, maxRV, "resource_version should be at least the latest RV we tracked") } // runTestCrossBackendConsistency tests basic consistency between SQL and KV backends (lightweight) diff --git a/pkg/storage/unified/testing/storage_backend_test.go b/pkg/storage/unified/testing/storage_backend_test.go index 092cd476b52..3046967adee 100644 --- a/pkg/storage/unified/testing/storage_backend_test.go +++ b/pkg/storage/unified/testing/storage_backend_test.go @@ -38,7 +38,6 @@ func TestBadgerKVStorageBackend(t *testing.T) { func TestSQLKVStorageBackend(t *testing.T) { skipTests := map[string]bool{ - TestHappyPath: true, TestWatchWriteEvents: true, TestList: true, TestBlobSupport: true, @@ -51,21 +50,24 @@ func TestSQLKVStorageBackend(t *testing.T) { TestGetResourceLastImportTime: true, TestOptimisticLocking: true, } - // without RvManager - RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend { - backend, _ := NewTestSqlKvBackend(t, ctx, false) - return backend - }, &TestOptions{ - NSPrefix: "sqlkvstorage-test", - SkipTests: skipTests, + + t.Run("Without RvManager", func(t *testing.T) { + RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend { + backend, _ := NewTestSqlKvBackend(t, ctx, false) + return backend + }, &TestOptions{ + NSPrefix: "sqlkvstorage-test", + SkipTests: skipTests, + }) }) - // with RvManager - RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend { - backend, _ := NewTestSqlKvBackend(t, ctx, true) - return backend - }, &TestOptions{ - NSPrefix: "sqlkvstorage-withrvmanager-test", - SkipTests: skipTests, + t.Run("With RvManager", func(t *testing.T) { + RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend { + backend, _ := NewTestSqlKvBackend(t, ctx, true) + return backend + }, &TestOptions{ + NSPrefix: "sqlkvstorage-withrvmanager-test", + SkipTests: skipTests, + }) }) } From 69bf3068b3423d7db4d5fc00279534bbb944c92a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ida=20=C5=A0tambuk?= Date: Mon, 12 Jan 2026 18:52:23 +0100 Subject: [PATCH 02/14] Dashboards: Never show scopes variables (#116132) --- .../scene/VariableControls.test.tsx | 84 +++++++++++++++++++ .../scene/VariableControls.tsx | 3 +- 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 public/app/features/dashboard-scene/scene/VariableControls.test.tsx diff --git a/public/app/features/dashboard-scene/scene/VariableControls.test.tsx b/public/app/features/dashboard-scene/scene/VariableControls.test.tsx new file mode 100644 index 00000000000..c65639edfa6 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/VariableControls.test.tsx @@ -0,0 +1,84 @@ +import { render, screen } from '@testing-library/react'; + +import { VariableHide } from '@grafana/data'; +import { SceneGridLayout, SceneVariable, SceneVariableSet, ScopesVariable, TextBoxVariable } from '@grafana/scenes'; + +import { DashboardScene } from './DashboardScene'; +import { VariableControls } from './VariableControls'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; + +jest.mock('@grafana/runtime', () => { + const runtime = jest.requireActual('@grafana/runtime'); + return { + ...runtime, + config: { + ...runtime.config, + featureToggles: { + dashboardNewLayouts: true, + }, + }, + }; +}); + +describe('VariableControls', () => { + it('should not render scopes variable', () => { + const variables = [new ScopesVariable({})]; + const dashboard = buildScene(variables); + dashboard.activate(); + + render(); + + expect(screen.queryByText('__scopes')).not.toBeInTheDocument(); + }); + + it('should not render regular hidden variables', () => { + const hiddenVariable = new TextBoxVariable({ + name: 'HiddenVar', + hide: VariableHide.hideVariable, + }); + const variables = [hiddenVariable]; + const dashboard = buildScene(variables); + dashboard.activate(); + + render(); + + expect(screen.queryByText('HiddenVar')).not.toBeInTheDocument(); + }); + + it('should render regular hidden variables in edit mode', async () => { + const hiddenVariable = new TextBoxVariable({ + name: 'HiddenVar', + hide: VariableHide.hideVariable, + }); + const variables = [hiddenVariable]; + const dashboard = buildScene(variables); + dashboard.activate(); + + dashboard.setState({ isEditing: true }); + render(); + + expect(await screen.findByText('HiddenVar')).toBeInTheDocument(); + }); + + it('should not render variables hidden in controls menu in edit mode', async () => { + const dashboard = buildScene([new TextBoxVariable({ name: 'TextVarControls', hide: VariableHide.inControlsMenu })]); + dashboard.activate(); + + dashboard.setState({ isEditing: true }); + render(); + + expect(screen.queryByText('TextVarControls')).not.toBeInTheDocument(); + }); +}); + +function buildScene(variables: SceneVariable[] = []) { + const dashboard = new DashboardScene({ + $variables: new SceneVariableSet({ variables }), + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [], + }), + }), + }); + return dashboard; +} diff --git a/public/app/features/dashboard-scene/scene/VariableControls.tsx b/public/app/features/dashboard-scene/scene/VariableControls.tsx index a2e6f3daf88..4cd92d34614 100644 --- a/public/app/features/dashboard-scene/scene/VariableControls.tsx +++ b/public/app/features/dashboard-scene/scene/VariableControls.tsx @@ -39,8 +39,9 @@ export function VariableControls({ dashboard }: { dashboard: DashboardScene }) { ? restVariables.filter((v) => v.state.hide !== VariableHide.inControlsMenu) : variables.filter( (v) => + // used for scopes variables, should always be hidden // if we're editing in dynamic dashboards, still shows hidden variable but greyed out - (isEditingNewLayouts && v.state.hide === VariableHide.hideVariable) || + (!v.UNSAFE_renderAsHidden && isEditingNewLayouts && v.state.hide === VariableHide.hideVariable) || v.state.hide !== VariableHide.inControlsMenu ); From 53aa5e8f7f19c19518876b0356d00d0f7ec42c85 Mon Sep 17 00:00:00 2001 From: Nick Richmond <5732000+NWRichmond@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:52:40 -0500 Subject: [PATCH 03/14] MetricsDrilldown: Remove `exploreMetricsRelatedLogs` feature toggle (#116090) chore: remove unused exploreMetricsRelatedLogs feature toggle --- packages/grafana-data/src/types/featureToggles.gen.ts | 4 ---- pkg/services/featuremgmt/registry.go | 8 -------- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.json | 3 ++- 4 files changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 31359af52cf..8bc45524b37 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -695,10 +695,6 @@ export interface FeatureToggles { */ passwordlessMagicLinkAuthentication?: boolean; /** - * Display Related Logs in Grafana Metrics Drilldown - */ - exploreMetricsRelatedLogs?: boolean; - /** * Adds support for quotes and special characters in label values for Prometheus queries */ prometheusSpecialCharsInLabelValues?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index f6d9e2da560..3e108d1ed0c 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1148,14 +1148,6 @@ var ( Owner: identityAccessTeam, HideFromDocs: true, }, - { - Name: "exploreMetricsRelatedLogs", - Description: "Display Related Logs in Grafana Metrics Drilldown", - Stage: FeatureStageExperimental, - Owner: grafanaObservabilityMetricsSquad, - FrontendOnly: true, - HideFromDocs: false, - }, { Name: "prometheusSpecialCharsInLabelValues", Description: "Adds support for quotes and special characters in label values for Prometheus queries", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index fdb265c7980..20ff391364e 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -159,7 +159,6 @@ newTimeRangeZoomShortcuts,experimental,@grafana/dataviz-squad,false,false,true azureMonitorDisableLogLimit,GA,@grafana/partner-datasources,false,false,false playlistsReconciler,experimental,@grafana/grafana-app-platform-squad,false,true,false passwordlessMagicLinkAuthentication,experimental,@grafana/identity-access-team,false,false,false -exploreMetricsRelatedLogs,experimental,@grafana/observability-metrics,false,false,true prometheusSpecialCharsInLabelValues,experimental,@grafana/oss-big-tent,false,false,true enableExtensionsAdminPage,experimental,@grafana/plugins-platform-backend,false,true,false enableSCIM,preview,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 5747b9bdd20..50383070847 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1408,7 +1408,8 @@ "metadata": { "name": "exploreMetricsRelatedLogs", "resourceVersion": "1764664939750", - "creationTimestamp": "2024-11-05T16:28:43Z" + "creationTimestamp": "2024-11-05T16:28:43Z", + "deletionTimestamp": "2026-01-09T22:14:53Z" }, "spec": { "description": "Display Related Logs in Grafana Metrics Drilldown", From 69ccfd6bfc9541c4c912a41aafc61025fb45f234 Mon Sep 17 00:00:00 2001 From: Will Assis <35489495+gassiss@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:33:34 -0500 Subject: [PATCH 04/14] unified-storage: fix sharedwithme search not returning folders (#116089) * unified-storage: fix dashboard sharedwithme search not returning folders shared with the user --- pkg/registry/apis/dashboard/search.go | 25 ++++++++++++------ pkg/registry/apis/dashboard/search_test.go | 30 +++++++++++++++++++--- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 8572dae8295..bbc032e7382 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -552,6 +552,7 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use // gets dashboards that the user was granted read access to permissions := user.GetPermissions() dashboardPermissions := permissions[dashboards.ActionDashboardsRead] + folderPermissions := permissions[dashboards.ActionFoldersRead] dashboardUids := make([]string, 0) sharedDashboards := make([]string, 0) @@ -562,6 +563,13 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use } } } + for _, folderPermission := range folderPermissions { + if folderUid, found := strings.CutPrefix(folderPermission, dashboards.ScopeFoldersPrefix); found { + if !slices.Contains(dashboardUids, folderUid) && folderUid != foldermodel.SharedWithMeFolderUID && folderUid != foldermodel.GeneralFolderUID { + dashboardUids = append(dashboardUids, folderUid) + } + } + } if len(dashboardUids) == 0 { return sharedDashboards, nil @@ -572,9 +580,15 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use return sharedDashboards, err } + folderKey, err := asResourceKey(user.GetNamespace(), folders.RESOURCE) + if err != nil { + return sharedDashboards, err + } + dashboardSearchRequest := &resourcepb.ResourceSearchRequest{ - Fields: []string{"folder"}, - Limit: int64(len(dashboardUids)), + Federated: []*resourcepb.ResourceKey{folderKey}, + Fields: []string{"folder"}, + Limit: int64(len(dashboardUids)), Options: &resourcepb.ListOptions{ Key: key, Fields: []*resourcepb.Requirement{{ @@ -610,12 +624,6 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use } } - // only folders the user has access to will be returned here - folderKey, err := asResourceKey(user.GetNamespace(), folders.RESOURCE) - if err != nil { - return sharedDashboards, err - } - folderSearchRequest := &resourcepb.ResourceSearchRequest{ Fields: []string{"folder"}, Limit: int64(len(allFolders)), @@ -628,6 +636,7 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use }}, }, } + // only folders the user has access to will be returned here foldersResult, err := s.client.Search(ctx, folderSearchRequest) if err != nil { return sharedDashboards, err diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index 3b9935f8247..c7defba3fea 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -507,6 +507,15 @@ func TestSearchHandlerSharedDashboards(t *testing.T) { []byte("publicfolder"), // folder uid }, }, + { + Key: &resourcepb.ResourceKey{ + Name: "sharedfolder", + Resource: "folder", + }, + Cells: [][]byte{ + []byte("privatefolder"), // folder uid + }, + }, }, }, } @@ -550,6 +559,15 @@ func TestSearchHandlerSharedDashboards(t *testing.T) { []byte("privatefolder"), // folder uid }, }, + { + Key: &resourcepb.ResourceKey{ + Name: "sharedfolder", + Resource: "folder", + }, + Cells: [][]byte{ + []byte("privatefolder"), // folder uid + }, + }, }, }, } @@ -571,6 +589,7 @@ func TestSearchHandlerSharedDashboards(t *testing.T) { allPermissions := make(map[int64]map[string][]string) permissions := make(map[string][]string) permissions[dashboards.ActionDashboardsRead] = []string{"dashboards:uid:dashboardinroot", "dashboards:uid:dashboardinprivatefolder", "dashboards:uid:dashboardinpublicfolder"} + permissions[dashboards.ActionFoldersRead] = []string{"folders:uid:sharedfolder"} allPermissions[1] = permissions // "Permissions" is where we store the uid of dashboards shared with the user req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test", OrgID: 1, Permissions: allPermissions})) @@ -581,14 +600,19 @@ func TestSearchHandlerSharedDashboards(t *testing.T) { // first call gets all dashboards user has permission for firstCall := mockClient.MockCalls[0] - assert.Equal(t, firstCall.Options.Fields[0].Values, []string{"dashboardinroot", "dashboardinprivatefolder", "dashboardinpublicfolder"}) + assert.Equal(t, firstCall.Options.Fields[0].Values, []string{"dashboardinroot", "dashboardinprivatefolder", "dashboardinpublicfolder", "sharedfolder"}) + // verify federated field is set to include folders + assert.NotNil(t, firstCall.Federated) + assert.Equal(t, 1, len(firstCall.Federated)) + assert.Equal(t, "folder.grafana.app", firstCall.Federated[0].Group) + assert.Equal(t, "folders", firstCall.Federated[0].Resource) // second call gets folders associated with the previous dashboards secondCall := mockClient.MockCalls[1] assert.Equal(t, secondCall.Options.Fields[0].Values, []string{"privatefolder", "publicfolder"}) - // lastly, search ONLY for dashboards user has permission to read that are within folders the user does NOT have + // lastly, search ONLY for dashboards and folders user has permission to read that are within folders the user does NOT have // permission to read thirdCall := mockClient.MockCalls[2] - assert.Equal(t, thirdCall.Options.Fields[0].Values, []string{"dashboardinprivatefolder"}) + assert.Equal(t, thirdCall.Options.Fields[0].Values, []string{"dashboardinprivatefolder", "sharedfolder"}) resp := rr.Result() defer func() { From 8c8efd2494967a7dfbf2443f64eea4ec362e95c2 Mon Sep 17 00:00:00 2001 From: Will Assis <35489495+gassiss@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:31:29 -0500 Subject: [PATCH 05/14] unified-storage: skip sqlkv/sqlbackend compatibility tests in sqlite (#116164) --- .../testing/storage_backend_sql_compatibility.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/storage/unified/testing/storage_backend_sql_compatibility.go b/pkg/storage/unified/testing/storage_backend_sql_compatibility.go index 9066a39221c..5e1423fa1ec 100644 --- a/pkg/storage/unified/testing/storage_backend_sql_compatibility.go +++ b/pkg/storage/unified/testing/storage_backend_sql_compatibility.go @@ -81,6 +81,12 @@ func RunSQLStorageBackendCompatibilityTest(t *testing.T, newSqlBackend, newKvBac kvbackend, db := newKvBackend(t.Context()) sqlbackend, _ := newSqlBackend(t.Context()) + + // Skip on SQLite due to concurrency limitations + if db.DriverName() == "sqlite3" { + t.Skip("Skipping concurrent operations stress test on SQLite") + } + tc.fn(t, sqlbackend, kvbackend, opts.NSPrefix, db) }) } @@ -686,11 +692,6 @@ func runTestCrossBackendConsistency(t *testing.T, sqlBackend, kvBackend resource // runTestConcurrentOperationsStress tests heavy concurrent operations between SQL and KV backends func runTestConcurrentOperationsStress(t *testing.T, sqlBackend, kvBackend resource.StorageBackend, nsPrefix string, db sqldb.DB) { - // Skip on SQLite due to concurrency limitations - if db.DriverName() == "sqlite3" { - t.Skip("Skipping concurrent operations stress test on SQLite") - } - ctx := testutil.NewDefaultTestContext(t) // Create storage servers from both backends From ce9ab6a89ad66d18ab2b6aa8e220a6db1e83a006 Mon Sep 17 00:00:00 2001 From: Denis Vodopianov Date: Mon, 12 Jan 2026 22:53:23 +0100 Subject: [PATCH 06/14] Add non-boolean feature flags support to the StaticProvider (#115085) * initial commit * add support of integerts * finialise the static provider * minor refactoring * the rest * revert: the rest * add new thiongs * more tests added * add ff parsing tests to check if types are handled correctly * update tests according to recent changes * address golint issues * Update pkg/setting/setting_feature_toggles.go Co-authored-by: Dave Henderson * fix rebase issues * addressing review comments * add test cases for enterprise * handle enterprise cases * minor refactoring to make api a bit easier to debug * make test names a bit more precise * fix linter * add openfeature sdk to goleak ignore in testutil * Remove only boolean check in ff gen tests * add non-boolean types top the doc in default.ini and doc string in FeatureFlag type * apply remarks, add docs to sample.ini * reflect changes in feature flags in the public grafana configuration doc * fix doc formatting * apply suggestions to the doc file --------- Co-authored-by: Dave Henderson --- conf/defaults.ini | 8 +- conf/sample.ini | 11 +- .../setup-grafana/configure-grafana/_index.md | 6 +- pkg/services/featuremgmt/models.go | 6 +- pkg/services/featuremgmt/openfeature.go | 7 +- pkg/services/featuremgmt/service.go | 3 +- pkg/services/featuremgmt/static_evaluator.go | 2 +- pkg/services/featuremgmt/static_provider.go | 47 +++--- .../featuremgmt/static_provider_test.go | 142 ++++++++++++++++++ pkg/services/featuremgmt/toggles_gen_test.go | 3 - pkg/services/updatemanager/plugins_test.go | 7 +- pkg/setting/setting_feature_toggles.go | 80 +++++++++- pkg/setting/setting_feature_toggles_test.go | 77 +++++++--- pkg/tests/apis/features/features_test.go | 2 +- pkg/util/testutil/context_test.go | 5 +- 15 files changed, 330 insertions(+), 76 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 8e0a113a0ec..080d4e62fe0 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -336,7 +336,7 @@ rudderstack_data_plane_url = rudderstack_sdk_url = # Rudderstack v3 SDK, optional, defaults to false. If set, Rudderstack v3 SDK will be used instead of v1 -rudderstack_v3_sdk_url = +rudderstack_v3_sdk_url = # Rudderstack Config url, optional, used by Rudderstack SDK to fetch source config rudderstack_config_url = @@ -2079,8 +2079,14 @@ enable = # To enable features by default, set `Expression: "true"` in: # https://github.com/grafana/grafana/blob/main/pkg/services/featuremgmt/registry.go +# The feature_toggles section supports feature flags of a number of types, +# including boolean, string, integer, float, and structured values, following the OpenFeature specification. +# # feature1 = true # feature2 = false +# feature3 = "foobar" +# feature4 = 1.5 +# feature5 = { "foo": "bar" } [feature_toggles.openfeature] # This is EXPERIMENTAL. Please, do not use this section diff --git a/conf/sample.ini b/conf/sample.ini index 5a579d0e74e..b4bc6027abf 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -323,7 +323,7 @@ ;rudderstack_sdk_url = # Rudderstack v3 SDK, optional, defaults to false. If set, Rudderstack v3 SDK will be used instead of v1 -;rudderstack_v3_sdk_url = +;rudderstack_v3_sdk_url = # Rudderstack Config url, optional, used by Rudderstack SDK to fetch source config ;rudderstack_config_url = @@ -1913,7 +1913,7 @@ default_datasource_uid = # client_queue_max_size is the maximum size in bytes of the client queue # for Live connections. Defaults to 4MB. -;client_queue_max_size = +;client_queue_max_size = #################################### Grafana Image Renderer Plugin ########################## [plugin.grafana-image-renderer] @@ -1996,9 +1996,14 @@ default_datasource_uid = ;enable = feature1,feature2 +# The feature_toggles section supports feature flags of a number of types, +# including boolean, string, integer, float, and structured values, following the OpenFeature specification. + ;feature1 = true ;feature2 = false - +;feature3 = "foobar" +;feature4 = 1.5 +;feature5 = { "foo": "bar" } [date_formats] # For information on what formatting patterns that are supported https://momentjs.com/docs/#/displaying/ diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 67c361b2bdc..d9125991c12 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -2836,9 +2836,11 @@ For more information about Grafana Enterprise, refer to [Grafana Enterprise](../ Keys of features to enable, separated by space. -#### `FEATURE_TOGGLE_NAME = false` +#### `FEATURE_NAME = ` -Some feature toggles for stable features are on by default. Use this setting to disable an on-by-default feature toggle with the name FEATURE_TOGGLE_NAME, for example, `exploreMixedDatasource = false`. +Use a key-value pair to set feature flag values explicitly, overriding any default values. A few different types are supported, following the OpenFeature specification. See the defaults.ini file for more details. + +For example, to disable an on-by-default feature toggle named `exploreMixedDatasource`, specify `exploreMixedDatasource = false`.
diff --git a/pkg/services/featuremgmt/models.go b/pkg/services/featuremgmt/models.go index d59dff63c37..72f9d5ffc5b 100644 --- a/pkg/services/featuremgmt/models.go +++ b/pkg/services/featuremgmt/models.go @@ -133,7 +133,11 @@ type FeatureFlag struct { Stage FeatureFlagStage `json:"stage,omitempty"` Owner codeowner `json:"-"` // Owner person or team that owns this feature flag - // CEL-GO expression. Using the value "true" will mean this is on by default + // Expression defined by the feature_toggles configuration. + // Supports multiple types including boolean, string, integer, float, + // and structured values following the OpenFeature specification. + // Using the value "true" means the feature flag is enabled by default, + // Using the value "1.0" means the default value of the feature flag is 1.0 Expression string `json:"expression,omitempty"` // Special behavior properties diff --git a/pkg/services/featuremgmt/openfeature.go b/pkg/services/featuremgmt/openfeature.go index cd3b77322fb..22017b8de03 100644 --- a/pkg/services/featuremgmt/openfeature.go +++ b/pkg/services/featuremgmt/openfeature.go @@ -8,6 +8,7 @@ import ( clientauthmiddleware "github.com/grafana/grafana/pkg/clientauth/middleware" "github.com/grafana/grafana/pkg/setting" + "github.com/open-feature/go-sdk/openfeature/memprovider" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/open-feature/go-sdk/openfeature" @@ -26,7 +27,7 @@ type OpenFeatureConfig struct { // HTTPClient is a pre-configured HTTP client (optional, used by features-service + OFREP providers) HTTPClient *http.Client // StaticFlags are the feature flags to use with static provider - StaticFlags map[string]bool + StaticFlags map[string]memprovider.InMemoryFlag // TargetingKey is used for evaluation context TargetingKey string // ContextAttrs are additional attributes for evaluation context @@ -100,7 +101,7 @@ func InitOpenFeatureWithCfg(cfg *setting.Cfg) error { func createProvider( providerType string, u *url.URL, - staticFlags map[string]bool, + staticFlags map[string]memprovider.InMemoryFlag, httpClient *http.Client, ) (openfeature.FeatureProvider, error) { if providerType == setting.FeaturesServiceProviderType || providerType == setting.OFREPProviderType { @@ -117,7 +118,7 @@ func createProvider( } } - return newStaticProvider(staticFlags) + return newStaticProvider(staticFlags, standardFeatureFlags) } func createHTTPClient(m *clientauthmiddleware.TokenExchangeMiddleware) (*http.Client, error) { diff --git a/pkg/services/featuremgmt/service.go b/pkg/services/featuremgmt/service.go index 2769a75d788..2c97666d9e9 100644 --- a/pkg/services/featuremgmt/service.go +++ b/pkg/services/featuremgmt/service.go @@ -47,7 +47,8 @@ func ProvideManagerService(cfg *setting.Cfg) (*FeatureManager, error) { } mgmt.warnings[key] = "unknown flag in config" } - mgmt.startup[key] = val + + mgmt.startup[key] = val.Variants[val.DefaultVariant] == true } // update the values diff --git a/pkg/services/featuremgmt/static_evaluator.go b/pkg/services/featuremgmt/static_evaluator.go index c3d46837d28..fdeef7a5858 100644 --- a/pkg/services/featuremgmt/static_evaluator.go +++ b/pkg/services/featuremgmt/static_evaluator.go @@ -29,7 +29,7 @@ func CreateStaticEvaluator(cfg *setting.Cfg) (StaticFlagEvaluator, error) { return nil, fmt.Errorf("failed to read feature flags from config: %w", err) } - staticProvider, err := newStaticProvider(staticFlags) + staticProvider, err := newStaticProvider(staticFlags, standardFeatureFlags) if err != nil { return nil, fmt.Errorf("failed to create static provider: %w", err) } diff --git a/pkg/services/featuremgmt/static_provider.go b/pkg/services/featuremgmt/static_provider.go index f384bd00de1..f6fe14d7de9 100644 --- a/pkg/services/featuremgmt/static_provider.go +++ b/pkg/services/featuremgmt/static_provider.go @@ -1,8 +1,13 @@ package featuremgmt import ( + "fmt" + "maps" + "github.com/open-feature/go-sdk/openfeature" "github.com/open-feature/go-sdk/openfeature/memprovider" + + "github.com/grafana/grafana/pkg/setting" ) // inMemoryBulkProvider is a wrapper around memprovider.InMemoryProvider that @@ -28,37 +33,21 @@ func (p *inMemoryBulkProvider) ListFlags() ([]string, error) { return keys, nil } -func newStaticProvider(confFlags map[string]bool) (openfeature.FeatureProvider, error) { - flags := make(map[string]memprovider.InMemoryFlag, len(standardFeatureFlags)) +func newStaticProvider(confFlags map[string]memprovider.InMemoryFlag, standardFlags []FeatureFlag) (openfeature.FeatureProvider, error) { + flags := make(map[string]memprovider.InMemoryFlag, len(standardFlags)) + + // Parse and add standard flags + for _, flag := range standardFlags { + inMemFlag, err := setting.ParseFlag(flag.Name, flag.Expression) + if err != nil { + return nil, fmt.Errorf("failed to parse flag %s: %w", flag.Name, err) + } + + flags[flag.Name] = inMemFlag + } // Add flags from config.ini file - for name, value := range confFlags { - flags[name] = createInMemoryFlag(name, value) - } - - // Add standard flags - for _, flag := range standardFeatureFlags { - if _, exists := flags[flag.Name]; !exists { - enabled := flag.Expression == "true" - flags[flag.Name] = createInMemoryFlag(flag.Name, enabled) - } - } + maps.Copy(flags, confFlags) return newInMemoryBulkProvider(flags), nil } - -func createInMemoryFlag(name string, enabled bool) memprovider.InMemoryFlag { - variant := "disabled" - if enabled { - variant = "enabled" - } - - return memprovider.InMemoryFlag{ - Key: name, - DefaultVariant: variant, - Variants: map[string]interface{}{ - "enabled": true, - "disabled": false, - }, - } -} diff --git a/pkg/services/featuremgmt/static_provider_test.go b/pkg/services/featuremgmt/static_provider_test.go index 29610baa8c4..c3245f7dd97 100644 --- a/pkg/services/featuremgmt/static_provider_test.go +++ b/pkg/services/featuremgmt/static_provider_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/setting" + "github.com/open-feature/go-sdk/openfeature/memprovider" "github.com/open-feature/go-sdk/openfeature" "github.com/stretchr/testify/assert" @@ -93,3 +94,144 @@ ABCD = true enabledFeatureManager := mgr.GetEnabled(ctx) assert.Equal(t, openFeatureEnabledFlags, enabledFeatureManager) } + +func Test_StaticProvider_TypedFlags(t *testing.T) { + tests := []struct { + flags FeatureFlag + defaultValue any + expectedValue any + }{ + { + flags: FeatureFlag{ + Name: "Flag", + Expression: "true", + }, + defaultValue: false, + expectedValue: true, + }, + { + flags: FeatureFlag{ + Name: "Flag", + Expression: "1.0", + }, + defaultValue: 0.0, + expectedValue: 1.0, + }, + { + flags: FeatureFlag{ + Name: "Flag", + Expression: "blue", + }, + defaultValue: "red", + expectedValue: "blue", + }, + { + flags: FeatureFlag{ + Name: "Flag", + Expression: "1", + }, + defaultValue: int64(0), + expectedValue: int64(1), + }, + { + flags: FeatureFlag{ + Name: "Flag", + Expression: `{ "foo": "bar" }`, + }, + expectedValue: map[string]any{"foo": "bar"}, + }, + } + + for _, tt := range tests { + provider, err := newStaticProvider(nil, []FeatureFlag{tt.flags}) + assert.NoError(t, err) + + var result any + switch tt.expectedValue.(type) { + case bool: + result = provider.BooleanEvaluation(t.Context(), tt.flags.Name, tt.defaultValue.(bool), openfeature.FlattenedContext{}).Value + case float64: + result = provider.FloatEvaluation(t.Context(), tt.flags.Name, tt.defaultValue.(float64), openfeature.FlattenedContext{}).Value + case string: + result = provider.StringEvaluation(t.Context(), tt.flags.Name, tt.defaultValue.(string), openfeature.FlattenedContext{}).Value + case int64: + result = provider.IntEvaluation(t.Context(), tt.flags.Name, tt.defaultValue.(int64), openfeature.FlattenedContext{}).Value + case map[string]any: + result = provider.ObjectEvaluation(t.Context(), tt.flags.Name, tt.defaultValue, openfeature.FlattenedContext{}).Value + } + + assert.Equal(t, tt.expectedValue, result) + } +} +func Test_StaticProvider_ConfigOverride(t *testing.T) { + tests := []struct { + name string + originalValue string + configValue any + }{ + { + name: "bool", + originalValue: "false", + configValue: true, + }, + { + name: "int", + originalValue: "0", + configValue: int64(1), + }, + { + name: "float", + originalValue: "0.0", + configValue: 1.0, + }, + { + name: "string", + originalValue: "foo", + configValue: "bar", + }, + { + name: "structure", + originalValue: "{}", + configValue: make(map[string]any), + }, + } + + for _, tt := range tests { + configFlags, standardFlags := makeFlags(tt) + provider, err := newStaticProvider(configFlags, standardFlags) + assert.NoError(t, err) + + var result any + switch tt.configValue.(type) { + case bool: + result = provider.BooleanEvaluation(t.Context(), tt.name, false, openfeature.FlattenedContext{}).Value + case float64: + result = provider.FloatEvaluation(t.Context(), tt.name, 0.0, openfeature.FlattenedContext{}).Value + case string: + result = provider.StringEvaluation(t.Context(), tt.name, "foo", openfeature.FlattenedContext{}).Value + case int64: + result = provider.IntEvaluation(t.Context(), tt.name, 1, openfeature.FlattenedContext{}).Value + case map[string]any: + result = provider.ObjectEvaluation(t.Context(), tt.name, make(map[string]any), openfeature.FlattenedContext{}).Value + } + + assert.Equal(t, tt.configValue, result) + } +} + +func makeFlags(tt struct { + name string + originalValue string + configValue any +}) (map[string]memprovider.InMemoryFlag, []FeatureFlag) { + orig := FeatureFlag{ + Name: tt.name, + Expression: tt.originalValue, + } + + config := map[string]memprovider.InMemoryFlag{ + tt.name: setting.NewInMemoryFlag(tt.name, tt.configValue), + } + + return config, []FeatureFlag{orig} +} diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 57e308de4ec..dbfa4af0c1c 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -190,9 +190,6 @@ func verifyFlagsConfiguration(t *testing.T) { if flag.Stage == FeatureStageGeneralAvailability && flag.Expression == "" { t.Errorf("GA features must be explicitly enabled or disabled, please add the `Expression` property for %s", flag.Name) } - if flag.Expression != "" && flag.Expression != "true" && flag.Expression != "false" { - t.Errorf("the `Expression` property for %s is incorrect. valid values are: `true`, `false` or empty string for default", flag.Name) - } // Check camel case names if flag.Name != strcase.ToLowerCamel(flag.Name) && !legacyNames[flag.Name] { invalidNames = append(invalidNames, flag.Name) diff --git a/pkg/services/updatemanager/plugins_test.go b/pkg/services/updatemanager/plugins_test.go index 75834c44c7d..93b3d54b321 100644 --- a/pkg/services/updatemanager/plugins_test.go +++ b/pkg/services/updatemanager/plugins_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/openfeature/memprovider" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" @@ -378,8 +379,10 @@ func setupOpenFeatureProvider(t *testing.T, flagValue bool) { err := featuremgmt.InitOpenFeature(featuremgmt.OpenFeatureConfig{ ProviderType: setting.StaticProviderType, - StaticFlags: map[string]bool{ - featuremgmt.FlagPluginsAutoUpdate: flagValue, + StaticFlags: map[string]memprovider.InMemoryFlag{ + featuremgmt.FlagPluginsAutoUpdate: { + Key: featuremgmt.FlagPluginsAutoUpdate, Variants: map[string]any{"": flagValue}, + }, }, }) require.NoError(t, err) diff --git a/pkg/setting/setting_feature_toggles.go b/pkg/setting/setting_feature_toggles.go index e09b45e9edb..38bfd0269e1 100644 --- a/pkg/setting/setting_feature_toggles.go +++ b/pkg/setting/setting_feature_toggles.go @@ -1,13 +1,20 @@ package setting import ( + "encoding/json" + "math" "strconv" "gopkg.in/ini.v1" + "github.com/open-feature/go-sdk/openfeature/memprovider" + "github.com/grafana/grafana/pkg/util" ) +// DefaultVariantName a placeholder name for config-based Feature Flags +const DefaultVariantName = "default" + // Deprecated: should use `featuremgmt.FeatureToggles` func (cfg *Cfg) readFeatureToggles(iniFile *ini.File) error { section := iniFile.Section("feature_toggles") @@ -15,18 +22,27 @@ func (cfg *Cfg) readFeatureToggles(iniFile *ini.File) error { if err != nil { return err } + // TODO IsFeatureToggleEnabled has been deprecated for 2 years now, we should remove this function completely // nolint:staticcheck - cfg.IsFeatureToggleEnabled = func(key string) bool { return toggles[key] } + cfg.IsFeatureToggleEnabled = func(key string) bool { + toggle, ok := toggles[key] + if !ok { + return false + } + + value, ok := toggle.Variants[toggle.DefaultVariant].(bool) + return value && ok + } return nil } -func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]bool, error) { - featureToggles := make(map[string]bool, 10) +func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]memprovider.InMemoryFlag, error) { + featureToggles := make(map[string]memprovider.InMemoryFlag, 10) // parse the comma separated list in `enable`. featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "") for _, feature := range util.SplitString(featuresTogglesStr) { - featureToggles[feature] = true + featureToggles[feature] = memprovider.InMemoryFlag{Key: feature, DefaultVariant: DefaultVariantName, Variants: map[string]any{DefaultVariantName: true}} } // read all other settings under [feature_toggles]. If a toggle is @@ -36,7 +52,7 @@ func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[str continue } - b, err := strconv.ParseBool(v.Value()) + b, err := ParseFlag(v.Name(), v.Value()) if err != nil { return featureToggles, err } @@ -45,3 +61,57 @@ func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[str } return featureToggles, nil } + +func ParseFlag(name, value string) (memprovider.InMemoryFlag, error) { + var structure map[string]any + + if integer, err := strconv.Atoi(value); err == nil { + return NewInMemoryFlag(name, integer), nil + } + if float, err := strconv.ParseFloat(value, 64); err == nil { + return NewInMemoryFlag(name, float), nil + } + if err := json.Unmarshal([]byte(value), &structure); err == nil { + return NewInMemoryFlag(name, structure), nil + } + if boolean, err := strconv.ParseBool(value); err == nil { + return NewInMemoryFlag(name, boolean), nil + } + + return NewInMemoryFlag(name, value), nil +} + +func NewInMemoryFlag(name string, value any) memprovider.InMemoryFlag { + return memprovider.InMemoryFlag{Key: name, DefaultVariant: DefaultVariantName, Variants: map[string]any{DefaultVariantName: value}} +} + +func AsStringMap(m map[string]memprovider.InMemoryFlag) map[string]string { + var res = map[string]string{} + for k, v := range m { + res[k] = serializeFlagValue(v) + } + return res +} + +func serializeFlagValue(flag memprovider.InMemoryFlag) string { + value := flag.Variants[flag.DefaultVariant] + + switch castedValue := value.(type) { + case bool: + return strconv.FormatBool(castedValue) + case int64: + return strconv.FormatInt(castedValue, 10) + case float64: + // handle cases with a single or no zeros after the decimal point + if math.Trunc(castedValue) == castedValue { + return strconv.FormatFloat(castedValue, 'f', 1, 64) + } + + return strconv.FormatFloat(castedValue, 'g', -1, 64) + case string: + return castedValue + default: + val, _ := json.Marshal(value) + return string(val) + } +} diff --git a/pkg/setting/setting_feature_toggles_test.go b/pkg/setting/setting_feature_toggles_test.go index b0c3730bcad..040a9ef7427 100644 --- a/pkg/setting/setting_feature_toggles_test.go +++ b/pkg/setting/setting_feature_toggles_test.go @@ -1,9 +1,11 @@ package setting import ( - "strconv" "testing" + "github.com/google/go-cmp/cmp" + "github.com/open-feature/go-sdk/openfeature/memprovider" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/ini.v1" ) @@ -12,17 +14,16 @@ func TestFeatureToggles(t *testing.T) { testCases := []struct { name string conf map[string]string - err error - expectedToggles map[string]bool + expectedToggles map[string]memprovider.InMemoryFlag }{ { name: "can parse feature toggles passed in the `enable` array", conf: map[string]string{ "enable": "feature1,feature2", }, - expectedToggles: map[string]bool{ - "feature1": true, - "feature2": true, + expectedToggles: map[string]memprovider.InMemoryFlag{ + "feature1": NewInMemoryFlag("feature1", true), + "feature2": NewInMemoryFlag("feature2", true), }, }, { @@ -31,10 +32,10 @@ func TestFeatureToggles(t *testing.T) { "enable": "feature1,feature2", "feature3": "true", }, - expectedToggles: map[string]bool{ - "feature1": true, - "feature2": true, - "feature3": true, + expectedToggles: map[string]memprovider.InMemoryFlag{ + "feature1": NewInMemoryFlag("feature1", true), + "feature2": NewInMemoryFlag("feature2", true), + "feature3": NewInMemoryFlag("feature3", true), }, }, { @@ -43,19 +44,26 @@ func TestFeatureToggles(t *testing.T) { "enable": "feature1,feature2", "feature2": "false", }, - expectedToggles: map[string]bool{ - "feature1": true, - "feature2": false, + expectedToggles: map[string]memprovider.InMemoryFlag{ + "feature1": NewInMemoryFlag("feature1", true), + "feature2": NewInMemoryFlag("feature2", false), }, }, { - name: "invalid boolean value should return syntax error", + name: "feature flags of different types are handled correctly", conf: map[string]string{ - "enable": "feature1,feature2", - "feature2": "invalid", + "feature1": "1", "feature2": "1.0", + "feature3": `{"foo":"bar"}`, "feature4": "bar", + "feature5": "t", "feature6": "T", + }, + expectedToggles: map[string]memprovider.InMemoryFlag{ + "feature1": NewInMemoryFlag("feature1", 1), + "feature2": NewInMemoryFlag("feature2", 1.0), + "feature3": NewInMemoryFlag("feature3", map[string]any{"foo": "bar"}), + "feature4": NewInMemoryFlag("feature4", "bar"), + "feature5": NewInMemoryFlag("feature5", true), + "feature6": NewInMemoryFlag("feature6", true), }, - expectedToggles: map[string]bool{}, - err: strconv.ErrSyntax, }, } @@ -69,12 +77,35 @@ func TestFeatureToggles(t *testing.T) { } featureToggles, err := ReadFeatureTogglesFromInitFile(toggles) - require.ErrorIs(t, err, tc.err) + require.NoError(t, err) - if err == nil { - for k, v := range featureToggles { - require.Equal(t, tc.expectedToggles[k], v, tc.name) - } + for k, v := range featureToggles { + toggle := tc.expectedToggles[k] + require.Equal(t, toggle, v, tc.name) + } + } +} + +func TestFlagValueSerialization(t *testing.T) { + testCases := []memprovider.InMemoryFlag{ + NewInMemoryFlag("int", 1), + NewInMemoryFlag("1.0f", 1.0), + NewInMemoryFlag("1.01f", 1.01), + NewInMemoryFlag("1.10f", 1.10), + NewInMemoryFlag("struct", map[string]any{"foo": "bar"}), + NewInMemoryFlag("string", "bar"), + NewInMemoryFlag("true", true), + NewInMemoryFlag("false", false), + } + + for _, tt := range testCases { + asStringMap := AsStringMap(map[string]memprovider.InMemoryFlag{tt.Key: tt}) + + deserialized, err := ParseFlag(tt.Key, asStringMap[tt.Key]) + assert.NoError(t, err) + + if diff := cmp.Diff(tt, deserialized); diff != "" { + t.Errorf("(-want, +got) = %v", diff) } } } diff --git a/pkg/tests/apis/features/features_test.go b/pkg/tests/apis/features/features_test.go index ab7bfea64d4..0a8c9564492 100644 --- a/pkg/tests/apis/features/features_test.go +++ b/pkg/tests/apis/features/features_test.go @@ -44,6 +44,6 @@ func TestIntegrationFeatures(t *testing.T) { "value": true, "key":"`+flag+`", "reason":"static provider evaluation result", - "variant":"enabled"}`, string(rsp.Body)) + "variant":"default"}`, string(rsp.Body)) }) } diff --git a/pkg/util/testutil/context_test.go b/pkg/util/testutil/context_test.go index 4d7ecf670f6..ca5c5abee4f 100644 --- a/pkg/util/testutil/context_test.go +++ b/pkg/util/testutil/context_test.go @@ -15,7 +15,10 @@ import ( func TestMain(m *testing.M) { // make sure we don't leak goroutines after tests in this package have // finished, which means we haven't leaked contexts either - goleak.VerifyTestMain(m) + // (Except for goroutines running specific functions. If possible we should fix this.) + goleak.VerifyTestMain(m, + goleak.IgnoreTopFunction("github.com/open-feature/go-sdk/openfeature.(*eventExecutor).startEventListener.func1.1"), + ) } func TestTestContextFunc(t *testing.T) { From d0217588a3a6be271e04c7194a9be0f452449bdf Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:43:01 -0600 Subject: [PATCH 07/14] LogsDrilldown: Remove exploreLogsLimitedTimeRange flag (#116177) chore: remove flag --- 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.json | 3 ++- 4 files changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 8bc45524b37..f1b1ce4154d 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -622,10 +622,6 @@ export interface FeatureToggles { */ exploreLogsAggregatedMetrics?: boolean; /** - * Used in Logs Drilldown to limit the time range - */ - exploreLogsLimitedTimeRange?: boolean; - /** * Enables the gRPC client to authenticate with the App Platform by using ID & access tokens */ appPlatformGrpcClientAuth?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 3e108d1ed0c..c9f2ca03184 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1031,13 +1031,6 @@ var ( FrontendOnly: true, Owner: grafanaObservabilityLogsSquad, }, - { - Name: "exploreLogsLimitedTimeRange", - Description: "Used in Logs Drilldown to limit the time range", - Stage: FeatureStageExperimental, - FrontendOnly: true, - Owner: grafanaObservabilityLogsSquad, - }, { Name: "appPlatformGrpcClientAuth", Description: "Enables the gRPC client to authenticate with the App Platform by using ID & access tokens", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 20ff391364e..7557323e43b 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -142,7 +142,6 @@ vizActionsAuth,preview,@grafana/dataviz-squad,false,false,true alertingPrometheusRulesPrimary,experimental,@grafana/alerting-squad,false,false,true exploreLogsShardSplitting,experimental,@grafana/observability-logs,false,false,true exploreLogsAggregatedMetrics,experimental,@grafana/observability-logs,false,false,true -exploreLogsLimitedTimeRange,experimental,@grafana/observability-logs,false,false,true appPlatformGrpcClientAuth,experimental,@grafana/identity-access-team,false,false,false groupAttributeSync,privatePreview,@grafana/identity-access-team,false,false,false alertingQueryAndExpressionsStepMode,GA,@grafana/alerting-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 50383070847..49cdbd86374 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1382,7 +1382,8 @@ "metadata": { "name": "exploreLogsLimitedTimeRange", "resourceVersion": "1764664939750", - "creationTimestamp": "2024-08-29T13:55:59Z" + "creationTimestamp": "2024-08-29T13:55:59Z", + "deletionTimestamp": "2026-01-12T22:18:14Z" }, "spec": { "description": "Used in Logs Drilldown to limit the time range", From b57ed324843443c90d09552993bb23d3ceb0c8c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 13 Jan 2026 06:23:21 +0100 Subject: [PATCH 08/14] chore: remove app/core/config barrel files (#116068) --- eslint-suppressions.json | 5 ----- .../SplitPaneWrapper/SplitPaneWrapper.tsx | 2 +- public/app/core/config.ts | 1 - public/app/core/internationalization/dates.ts | 2 +- public/app/core/services/theme.ts | 3 +-- public/app/features/alerting/routes.tsx | 2 +- .../alerting/state/ThresholdMapper.ts | 2 +- .../unified/components/rule-editor/util.ts | 2 +- .../annotations/standardAnnotationSupport.ts | 2 +- .../auth-config/AuthProvidersListPage.tsx | 3 +-- public/app/features/canvas/element.ts | 2 +- public/app/features/canvas/elements/cloud.tsx | 2 +- .../app/features/canvas/elements/ellipse.tsx | 2 +- .../canvas/elements/parallelogram.tsx | 2 +- .../app/features/canvas/elements/triangle.tsx | 2 +- .../app/features/canvas/runtime/element.tsx | 2 +- public/app/features/canvas/runtime/scene.tsx | 3 +-- .../canvas/runtime/sceneAbleManagement.ts | 2 +- .../sharing/ShareButton/ShareMenu.test.tsx | 2 +- .../components/ShareModal/ShareModal.tsx | 2 +- .../dashgrid/PanelLoadTimeMonitor.test.tsx | 2 +- .../dashgrid/PanelLoadTimeMonitor.tsx | 2 +- .../dashgrid/panelOptionsLogger.test.ts | 2 +- .../dashboard/dashgrid/panelOptionsLogger.ts | 2 +- public/app/features/dashboard/routes.ts | 2 +- .../features/dashboard/services/TimeSrv.ts | 3 +-- .../dashboard/state/DashboardMigrator.test.ts | 2 +- .../dashboard/utils/loadSnapshotData.ts | 2 +- .../editors/ResourcePickerPopover.tsx | 3 +-- .../RawPrometheus/RawPrometheusContainer.tsx | 3 +-- .../features/explore/Table/TableContainer.tsx | 3 +-- .../TracePageHeader/TracePageHeader.test.tsx | 22 +++++-------------- .../TracePageHeader/TracePageHeader.tsx | 9 ++++++-- public/app/features/expressions/types.ts | 2 +- .../app/features/inspector/InspectDataTab.tsx | 3 +-- public/app/features/inspector/styles.ts | 2 +- public/app/features/panel/state/util.ts | 2 +- .../suggestions/getAllSuggestions.test.ts | 2 +- .../features/plugins/admin/state/actions.ts | 3 +-- public/app/features/profile/routes.tsx | 2 +- .../features/users/UsersActionBar.test.tsx | 2 +- .../datasource/alertmanager/ConfigEditor.tsx | 2 +- .../panel/alertlist/UnifiedAlertList.tsx | 3 +-- .../plugins/panel/bargauge/BarGaugePanel.tsx | 2 +- .../panel/candlestick/CandlestickPanel.tsx | 3 +-- .../canvas/components/CanvasContextMenu.tsx | 2 +- .../components/connections/ConnectionSVG.tsx | 2 +- .../components/connections/ConnectionSVG2.tsx | 2 +- public/app/plugins/panel/canvas/utils.ts | 3 ++- public/app/plugins/panel/gauge/GaugePanel.tsx | 2 +- .../panel/geomap/components/DebugOverlay.tsx | 2 +- .../panel/geomap/components/MarkersLegend.tsx | 2 +- .../geomap/components/MeasureOverlay.tsx | 2 +- .../plugins/panel/geomap/layers/registry.ts | 3 ++- .../plugins/panel/live/LiveChannelEditor.tsx | 2 +- .../panel/radialbar/RadialBarPanel.tsx | 3 +-- public/app/plugins/panel/table/suggestions.ts | 2 +- public/app/plugins/panel/text/module.tsx | 2 +- .../panel/timeseries/TimeSeriesPanel.tsx | 3 +-- 59 files changed, 69 insertions(+), 93 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 250dbd20348..f633ed5b4eb 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1156,11 +1156,6 @@ "count": 2 } }, - "public/app/core/config.ts": { - "no-barrel-files/no-barrel-files": { - "count": 2 - } - }, "public/app/core/navigation/types.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 diff --git a/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx b/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx index 9ff66c518ee..a6a9041f6c1 100644 --- a/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx +++ b/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx @@ -4,8 +4,8 @@ import * as React from 'react'; import SplitPane, { Split } from 'react-split-pane'; import { GrafanaTheme2 } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { getDragStyles } from '@grafana/ui'; -import { config } from 'app/core/config'; interface Props { splitOrientation?: Split; diff --git a/public/app/core/config.ts b/public/app/core/config.ts index 6c757c9436a..f725aab5472 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -1,6 +1,5 @@ import { PluginState } from '@grafana/data'; import { config, GrafanaBootConfig } from '@grafana/runtime'; -export { config, type GrafanaBootConfig as Settings }; let grafanaConfig: GrafanaBootConfig = config; diff --git a/public/app/core/internationalization/dates.ts b/public/app/core/internationalization/dates.ts index 9ef7bbbdb82..c151e505487 100644 --- a/public/app/core/internationalization/dates.ts +++ b/public/app/core/internationalization/dates.ts @@ -2,7 +2,7 @@ import deepEqual from 'fast-deep-equal'; import memoize from 'micro-memoize'; import { getLanguage } from '@grafana/i18n/internal'; -import { config } from 'app/core/config'; +import { config } from '@grafana/runtime'; const deepMemoize: typeof memoize = (fn) => memoize(fn, { isEqual: deepEqual }); diff --git a/public/app/core/services/theme.ts b/public/app/core/services/theme.ts index 542aa1ab743..82bd949904f 100644 --- a/public/app/core/services/theme.ts +++ b/public/app/core/services/theme.ts @@ -1,8 +1,7 @@ import { getThemeById } from '@grafana/data/internal'; -import { ThemeChangedEvent } from '@grafana/runtime'; +import { config, ThemeChangedEvent } from '@grafana/runtime'; import { appEvents } from '../app_events'; -import { config } from '../config'; import { contextSrv } from '../services/context_srv'; import { PreferencesService } from './PreferencesService'; diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index 34459b9581e..418327face5 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -1,7 +1,7 @@ import { Navigate } from 'react-router-dom-v5-compat'; +import { config } from '@grafana/runtime'; import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; -import { config } from 'app/core/config'; import { GrafanaRouteComponent, RouteDescriptor } from 'app/core/navigation/types'; import { AccessControlAction } from 'app/types/accessControl'; diff --git a/public/app/features/alerting/state/ThresholdMapper.ts b/public/app/features/alerting/state/ThresholdMapper.ts index 48b9b7c656a..140330b9d06 100644 --- a/public/app/features/alerting/state/ThresholdMapper.ts +++ b/public/app/features/alerting/state/ThresholdMapper.ts @@ -1,4 +1,4 @@ -import { config } from 'app/core/config'; +import { config } from '@grafana/runtime'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; export const hiddenReducerTypes = ['percent_diff', 'percent_diff_abs']; diff --git a/public/app/features/alerting/unified/components/rule-editor/util.ts b/public/app/features/alerting/unified/components/rule-editor/util.ts index b8e20cba6b4..852a37bdf25 100644 --- a/public/app/features/alerting/unified/components/rule-editor/util.ts +++ b/public/app/features/alerting/unified/components/rule-editor/util.ts @@ -8,8 +8,8 @@ import { ThresholdsMode, isTimeSeriesFrames, } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { GraphThresholdsStyleMode } from '@grafana/schema'; -import { config } from 'app/core/config'; import { EvalFunction } from 'app/features/alerting/state/alertDef'; import { isExpressionQuery } from 'app/features/expressions/guards'; import { ClassicCondition, ExpressionQueryType } from 'app/features/expressions/types'; diff --git a/public/app/features/annotations/standardAnnotationSupport.ts b/public/app/features/annotations/standardAnnotationSupport.ts index 90d28715c7f..cd258e5b0ee 100644 --- a/public/app/features/annotations/standardAnnotationSupport.ts +++ b/public/app/features/annotations/standardAnnotationSupport.ts @@ -18,7 +18,7 @@ import { standardTransformers, } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { config } from 'app/core/config'; +import { config } from '@grafana/runtime'; export const standardAnnotationSupport: AnnotationSupport = { /** diff --git a/public/app/features/auth-config/AuthProvidersListPage.tsx b/public/app/features/auth-config/AuthProvidersListPage.tsx index af05c498ccf..413d78c75a4 100644 --- a/public/app/features/auth-config/AuthProvidersListPage.tsx +++ b/public/app/features/auth-config/AuthProvidersListPage.tsx @@ -3,10 +3,9 @@ import { connect, ConnectedProps } from 'react-redux'; import { GrafanaEdition } from '@grafana/data/internal'; import { Trans } from '@grafana/i18n'; -import { reportInteraction } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { Grid, TextLink, ToolbarButton } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; -import { config } from 'app/core/config'; import { StoreState } from 'app/types/store'; import { isOpenSourceBuildOrUnlicenced } from '../admin/EnterpriseAuthFeaturesCard'; diff --git a/public/app/features/canvas/element.ts b/public/app/features/canvas/element.ts index 7e78ad784e7..97de7a5fd5b 100644 --- a/public/app/features/canvas/element.ts +++ b/public/app/features/canvas/element.ts @@ -2,8 +2,8 @@ import { ComponentType } from 'react'; import { DataLink, RegistryItem, Action } from '@grafana/data'; import { PanelOptionsSupplier } from '@grafana/data/internal'; +import { config } from '@grafana/runtime'; import { ColorDimensionConfig, ScaleDimensionConfig, DirectionDimensionConfig } from '@grafana/schema'; -import { config } from 'app/core/config'; import { BackgroundConfig, Constraint, LineConfig, Placement } from 'app/plugins/panel/canvas/panelcfg.gen'; import { LineStyleConfig } from '../../plugins/panel/canvas/editor/LineStyleEditor'; diff --git a/public/app/features/canvas/elements/cloud.tsx b/public/app/features/canvas/elements/cloud.tsx index f215b4dfd9e..1c11f05818e 100644 --- a/public/app/features/canvas/elements/cloud.tsx +++ b/public/app/features/canvas/elements/cloud.tsx @@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { config } from 'app/core/config'; +import { config } from '@grafana/runtime'; import { DimensionContext } from 'app/features/dimensions/context'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; diff --git a/public/app/features/canvas/elements/ellipse.tsx b/public/app/features/canvas/elements/ellipse.tsx index d4d7f3fb23f..6659263cb09 100644 --- a/public/app/features/canvas/elements/ellipse.tsx +++ b/public/app/features/canvas/elements/ellipse.tsx @@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { config } from 'app/core/config'; +import { config } from '@grafana/runtime'; import { DimensionContext } from 'app/features/dimensions/context'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; diff --git a/public/app/features/canvas/elements/parallelogram.tsx b/public/app/features/canvas/elements/parallelogram.tsx index 2dc0df79799..dced8408bd4 100644 --- a/public/app/features/canvas/elements/parallelogram.tsx +++ b/public/app/features/canvas/elements/parallelogram.tsx @@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { config } from 'app/core/config'; +import { config } from '@grafana/runtime'; import { DimensionContext } from 'app/features/dimensions/context'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; diff --git a/public/app/features/canvas/elements/triangle.tsx b/public/app/features/canvas/elements/triangle.tsx index 35b00d05761..e335092a785 100644 --- a/public/app/features/canvas/elements/triangle.tsx +++ b/public/app/features/canvas/elements/triangle.tsx @@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { config } from 'app/core/config'; +import { config } from '@grafana/runtime'; import { DimensionContext } from 'app/features/dimensions/context'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; diff --git a/public/app/features/canvas/runtime/element.tsx b/public/app/features/canvas/runtime/element.tsx index f53008f9cb7..4a7c5ec8a7b 100644 --- a/public/app/features/canvas/runtime/element.tsx +++ b/public/app/features/canvas/runtime/element.tsx @@ -14,10 +14,10 @@ import { ActionType, } from '@grafana/data'; import { t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; import { TooltipDisplayMode } from '@grafana/schema'; import { ConfirmModal, VariablesInputModal } from '@grafana/ui'; import { LayerElement } from 'app/core/components/Layers/types'; -import { config } from 'app/core/config'; import { notFoundItem } from 'app/features/canvas/elements/notFound'; import { DimensionContext } from 'app/features/dimensions/context'; import { diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index ad217e45337..4b58709934f 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -6,7 +6,7 @@ import { BehaviorSubject, ReplaySubject, Subject, Subscription } from 'rxjs'; import Selecto from 'selecto'; import { AppEvents, PanelData, OneClickMode, ActionType } from '@grafana/data'; -import { locationService } from '@grafana/runtime'; +import { config, locationService } from '@grafana/runtime'; import { ColorDimensionConfig, ResourceDimensionConfig, @@ -17,7 +17,6 @@ import { DirectionDimensionConfig, } from '@grafana/schema'; import { Portal } from '@grafana/ui'; -import { config } from 'app/core/config'; import { DimensionContext } from 'app/features/dimensions/context'; import { getColorDimensionFromData, diff --git a/public/app/features/canvas/runtime/sceneAbleManagement.ts b/public/app/features/canvas/runtime/sceneAbleManagement.ts index 9af3e1a74e0..e8e3cb7f3ac 100644 --- a/public/app/features/canvas/runtime/sceneAbleManagement.ts +++ b/public/app/features/canvas/runtime/sceneAbleManagement.ts @@ -2,7 +2,7 @@ import InfiniteViewer from 'infinite-viewer'; import Moveable from 'moveable'; import Selecto from 'selecto'; -import { config } from 'app/core/config'; +import { config } from '@grafana/runtime'; import { CONNECTION_ANCHOR_DIV_ID } from 'app/plugins/panel/canvas/components/connections/ConnectionAnchors'; import { CONNECTION_VERTEX_ID, diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx index c049579fb38..5c618b18b16 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx @@ -1,11 +1,11 @@ import { render, screen } from '@testing-library/react'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; +import { config } from '@grafana/runtime'; import { SceneTimeRange, VizPanel } from '@grafana/scenes'; import { contextSrv } from 'app/core/services/context_srv'; import { AccessControlAction } from 'app/types/accessControl'; -import { config } from '../../../../core/config'; import { grantUserPermissions } from '../../../alerting/unified/mocks'; import { DashboardScene, DashboardSceneState } from '../../scene/DashboardScene'; import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; diff --git a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx index 4c701cbda15..55df40308cf 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; import { Modal, ModalTabsHeader, TabContent, Themeable2, withTheme2 } from '@grafana/ui'; -import { config } from 'app/core/config'; import { contextSrv } from 'app/core/services/context_srv'; import { SharePublicDashboard } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard'; import { isPublicDashboardsEnabled } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; diff --git a/public/app/features/dashboard/dashgrid/PanelLoadTimeMonitor.test.tsx b/public/app/features/dashboard/dashgrid/PanelLoadTimeMonitor.test.tsx index 48358aa563c..550869201f1 100644 --- a/public/app/features/dashboard/dashgrid/PanelLoadTimeMonitor.test.tsx +++ b/public/app/features/dashboard/dashgrid/PanelLoadTimeMonitor.test.tsx @@ -4,7 +4,7 @@ const mockPushMeasurement = jest.fn(); import { PanelLoadTimeMonitor } from './PanelLoadTimeMonitor'; -jest.mock('app/core/config', () => ({ +jest.mock('@grafana/runtime', () => ({ config: { grafanaJavascriptAgent: { enabled: true, diff --git a/public/app/features/dashboard/dashgrid/PanelLoadTimeMonitor.tsx b/public/app/features/dashboard/dashgrid/PanelLoadTimeMonitor.tsx index 230add25c6a..2cda7cce69d 100644 --- a/public/app/features/dashboard/dashgrid/PanelLoadTimeMonitor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelLoadTimeMonitor.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { faro } from '@grafana/faro-web-sdk'; -import { config } from 'app/core/config'; +import { config } from '@grafana/runtime'; import { PanelLogEvents } from 'app/core/log_events'; interface Props { diff --git a/public/app/features/dashboard/dashgrid/panelOptionsLogger.test.ts b/public/app/features/dashboard/dashgrid/panelOptionsLogger.test.ts index 95a43c75cad..7c55a8842a2 100644 --- a/public/app/features/dashboard/dashgrid/panelOptionsLogger.test.ts +++ b/public/app/features/dashboard/dashgrid/panelOptionsLogger.test.ts @@ -12,7 +12,7 @@ jest.mock('@grafana/faro-web-sdk', () => ({ }, })); -jest.mock('app/core/config', () => ({ +jest.mock('@grafana/runtime', () => ({ config: { grafanaJavascriptAgent: { enabled: true, diff --git a/public/app/features/dashboard/dashgrid/panelOptionsLogger.ts b/public/app/features/dashboard/dashgrid/panelOptionsLogger.ts index 0e75d7fdad1..6c994f924cf 100644 --- a/public/app/features/dashboard/dashgrid/panelOptionsLogger.ts +++ b/public/app/features/dashboard/dashgrid/panelOptionsLogger.ts @@ -1,6 +1,6 @@ import { FieldConfigSource } from '@grafana/data'; import { faro } from '@grafana/faro-web-sdk'; -import { config } from 'app/core/config'; +import { config } from '@grafana/runtime'; import { FIELD_CONFIG_CUSTOM_KEY, FIELD_CONFIG_OVERRIDES_KEY, PanelLogEvents } from 'app/core/log_events'; interface PanelLogInfo { diff --git a/public/app/features/dashboard/routes.ts b/public/app/features/dashboard/routes.ts index 57702d1a35d..8cd75418deb 100644 --- a/public/app/features/dashboard/routes.ts +++ b/public/app/features/dashboard/routes.ts @@ -1,7 +1,7 @@ +import { config } from '@grafana/runtime'; import { DashboardRoutes } from 'app/types/dashboard'; import { SafeDynamicImport } from '../../core/components/DynamicImports/SafeDynamicImport'; -import { config } from '../../core/config'; import { RouteDescriptor } from '../../core/navigation/types'; export const getPublicDashboardRoutes = (): RouteDescriptor[] => { diff --git a/public/app/features/dashboard/services/TimeSrv.ts b/public/app/features/dashboard/services/TimeSrv.ts index fb8f346a267..2c819f9018f 100644 --- a/public/app/features/dashboard/services/TimeSrv.ts +++ b/public/app/features/dashboard/services/TimeSrv.ts @@ -13,10 +13,9 @@ import { dateTimeForTimeZone, } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { locationService } from '@grafana/runtime'; +import { config, locationService } from '@grafana/runtime'; import { sceneGraph } from '@grafana/scenes'; import { appEvents } from 'app/core/app_events'; -import { config } from 'app/core/config'; import { AutoRefreshInterval, contextSrv, ContextSrv } from 'app/core/services/context_srv'; import { getCopiedTimeRange, diff --git a/public/app/features/dashboard/state/DashboardMigrator.test.ts b/public/app/features/dashboard/state/DashboardMigrator.test.ts index acdc306fc87..6c4551f9158 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.test.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.test.ts @@ -2,8 +2,8 @@ import { each, map } from 'lodash'; import { DataLinkBuiltInVars, MappingType, VariableHide } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test'; +import { config } from '@grafana/runtime'; import { FieldConfigSource } from '@grafana/schema'; -import { config } from 'app/core/config'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; import { mockDataSource } from 'app/features/alerting/unified/mocks'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; diff --git a/public/app/features/dashboard/utils/loadSnapshotData.ts b/public/app/features/dashboard/utils/loadSnapshotData.ts index 2be1206ec12..d44e70e3ffb 100644 --- a/public/app/features/dashboard/utils/loadSnapshotData.ts +++ b/public/app/features/dashboard/utils/loadSnapshotData.ts @@ -6,7 +6,7 @@ import { LoadingState, PanelData, } from '@grafana/data'; -import { config } from 'app/core/config'; +import { config } from '@grafana/runtime'; import { SnapshotWorker } from '../../query/state/DashboardQueryRunner/SnapshotWorker'; import { getTimeSrv } from '../services/TimeSrv'; diff --git a/public/app/features/dimensions/editors/ResourcePickerPopover.tsx b/public/app/features/dimensions/editors/ResourcePickerPopover.tsx index bfb35f9e717..6187974620f 100644 --- a/public/app/features/dimensions/editors/ResourcePickerPopover.tsx +++ b/public/app/features/dimensions/editors/ResourcePickerPopover.tsx @@ -6,9 +6,8 @@ import { useRef, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; -import { getBackendSrv } from '@grafana/runtime'; +import { config, getBackendSrv } from '@grafana/runtime'; import { Button, useStyles2 } from '@grafana/ui'; -import { config } from 'app/core/config'; import { MediaType, PickerTabType, ResourceFolderName } from '../types'; diff --git a/public/app/features/explore/RawPrometheus/RawPrometheusContainer.tsx b/public/app/features/explore/RawPrometheus/RawPrometheusContainer.tsx index ebd005f595b..6a00d567a41 100644 --- a/public/app/features/explore/RawPrometheus/RawPrometheusContainer.tsx +++ b/public/app/features/explore/RawPrometheus/RawPrometheusContainer.tsx @@ -3,10 +3,9 @@ import { memo, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { applyFieldOverrides, DataFrame, SelectableValue, SplitOpen } from '@grafana/data'; -import { getTemplateSrv, reportInteraction } from '@grafana/runtime'; +import { config, getTemplateSrv, reportInteraction } from '@grafana/runtime'; import { TimeZone } from '@grafana/schema'; import { RadioButtonGroup, Table, AdHocFilterItem, PanelChrome } from '@grafana/ui'; -import { config } from 'app/core/config'; import { PANEL_BORDER } from 'app/core/constants'; import { ExploreItemState, TABLE_RESULTS_STYLE, TABLE_RESULTS_STYLES, TableResultsStyle } from 'app/types/explore'; import { StoreState } from 'app/types/store'; diff --git a/public/app/features/explore/Table/TableContainer.tsx b/public/app/features/explore/Table/TableContainer.tsx index 2c1614e539d..7283711e4ce 100644 --- a/public/app/features/explore/Table/TableContainer.tsx +++ b/public/app/features/explore/Table/TableContainer.tsx @@ -13,10 +13,9 @@ import { EventBusSrv, } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { getTemplateSrv, PanelRenderer } from '@grafana/runtime'; +import { config, getTemplateSrv, PanelRenderer } from '@grafana/runtime'; import { TimeZone } from '@grafana/schema'; import { AdHocFilterItem, PanelChrome, withTheme2, Themeable2, PanelContextProvider } from '@grafana/ui'; -import { config } from 'app/core/config'; import { hasDeprecatedParentRowIndex, migrateFromParentRowIndexToNestedFrames, diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx index fb735991a3d..037a4a3c0c2 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx @@ -22,7 +22,7 @@ import { PluginExtensionPoints, PluginExtensionTypes, } from '@grafana/data'; -import { usePluginLinks, usePluginComponents } from '@grafana/runtime'; +import { usePluginLinks, usePluginComponents, config } from '@grafana/runtime'; import { DEFAULT_SPAN_FILTERS } from 'app/features/explore/state/constants'; import { TraceViewPluginExtensionContext } from '../types/trace'; @@ -47,13 +47,6 @@ jest.mock('app/core/copy/appNotification', () => ({ })), })); -// Mock config -jest.mock('../../../../../core/config', () => ({ - config: { - feedbackLinksEnabled: false, // Default to false to avoid interference with tests - }, -})); - // Mock navigator.clipboard Object.assign(navigator, { clipboard: { @@ -127,6 +120,7 @@ describe('TracePageHeader test', () => { beforeEach(() => { jest.clearAllMocks(); mockWindowOpen.mockClear(); + config.feedbackLinksEnabled = false; // Default to false to avoid interference with tests }); it('should render the new trace header', () => { @@ -438,9 +432,7 @@ describe('TracePageHeader test', () => { }); it('should render feedback button when feedbackLinksEnabled is true', () => { - // Mock config with feedbackLinksEnabled = true - const mockConfig = require('../../../../../core/config'); - mockConfig.config.feedbackLinksEnabled = true; + config.feedbackLinksEnabled = true; setup(); @@ -453,9 +445,7 @@ describe('TracePageHeader test', () => { it('should display tooltip for feedback button', async () => { const user = userEvent.setup(); - // Mock config with feedbackLinksEnabled = true - const mockConfig = require('../../../../../core/config'); - mockConfig.config.feedbackLinksEnabled = true; + config.feedbackLinksEnabled = true; setup(); @@ -469,9 +459,7 @@ describe('TracePageHeader test', () => { }); it('should render feedback button with correct styling and icon', () => { - // Mock config with feedbackLinksEnabled = true - const mockConfig = require('../../../../../core/config'); - mockConfig.config.feedbackLinksEnabled = true; + config.feedbackLinksEnabled = true; setup(); diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx index f0038ff1593..1e0fedade74 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx @@ -26,7 +26,13 @@ import { PluginExtensionPoints, } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { reportInteraction, renderLimitedComponents, usePluginComponents, usePluginLinks } from '@grafana/runtime'; +import { + reportInteraction, + renderLimitedComponents, + usePluginComponents, + usePluginLinks, + config, +} from '@grafana/runtime'; import { AdHocFiltersComboboxRenderer } from '@grafana/scenes'; import { TimeZone } from '@grafana/schema'; import { @@ -46,7 +52,6 @@ import { } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; -import { config } from '../../../../../core/config'; import { downloadTraceAsJson } from '../../../../inspector/utils/download'; import { ViewRangeTimeUpdate, TUpdateViewRangeTimeFunction, ViewRange } from '../TraceTimelineViewer/types'; import { getHeaderTags, getTraceName } from '../model/trace-viewer'; diff --git a/public/app/features/expressions/types.ts b/public/app/features/expressions/types.ts index 3a4bd936424..ec83f0e50e2 100644 --- a/public/app/features/expressions/types.ts +++ b/public/app/features/expressions/types.ts @@ -1,5 +1,5 @@ import { DataQuery, ReducerID, SelectableValue } from '@grafana/data'; -import { config } from 'app/core/config'; +import { config } from '@grafana/runtime'; import { EvalFunction } from '../alerting/state/alertDef'; diff --git a/public/app/features/inspector/InspectDataTab.tsx b/public/app/features/inspector/InspectDataTab.tsx index 8aba53622cc..4e979740c9a 100644 --- a/public/app/features/inspector/InspectDataTab.tsx +++ b/public/app/features/inspector/InspectDataTab.tsx @@ -15,9 +15,8 @@ import { } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; -import { getTemplateSrv, reportInteraction } from '@grafana/runtime'; +import { config, getTemplateSrv, reportInteraction } from '@grafana/runtime'; import { Button, Spinner, Table } from '@grafana/ui'; -import { config } from 'app/core/config'; import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner'; import { dataFrameToLogsModel } from '../logs/logsModel'; diff --git a/public/app/features/inspector/styles.ts b/public/app/features/inspector/styles.ts index 86646950382..65a50dd3816 100644 --- a/public/app/features/inspector/styles.ts +++ b/public/app/features/inspector/styles.ts @@ -1,8 +1,8 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { stylesFactory } from '@grafana/ui'; -import { config } from 'app/core/config'; /** @deprecated */ export const getPanelInspectorStyles = stylesFactory(() => { diff --git a/public/app/features/panel/state/util.ts b/public/app/features/panel/state/util.ts index 87f674479a8..4c785e67fe8 100644 --- a/public/app/features/panel/state/util.ts +++ b/public/app/features/panel/state/util.ts @@ -1,5 +1,5 @@ import { PanelPluginMeta, PluginState, unEscapeStringFromRegex } from '@grafana/data'; -import { config } from 'app/core/config'; +import { config } from '@grafana/runtime'; export function getAllPanelPluginMeta(): PanelPluginMeta[] { const allPanels = config.panels; diff --git a/public/app/features/panel/suggestions/getAllSuggestions.test.ts b/public/app/features/panel/suggestions/getAllSuggestions.test.ts index 8657824cc27..7631cbf95f9 100644 --- a/public/app/features/panel/suggestions/getAllSuggestions.test.ts +++ b/public/app/features/panel/suggestions/getAllSuggestions.test.ts @@ -11,6 +11,7 @@ import { toDataFrame, VisualizationSuggestionScore, } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { BarGaugeDisplayMode, BigValueColorMode, @@ -20,7 +21,6 @@ import { VizOrientation, } from '@grafana/schema'; import { appEvents } from 'app/core/app_events'; -import { config } from 'app/core/config'; import { clearPanelPluginCache } from 'app/features/plugins/importPanelPlugin'; import { pluginImporter } from 'app/features/plugins/importer/pluginImporter'; diff --git a/public/app/features/plugins/admin/state/actions.ts b/public/app/features/plugins/admin/state/actions.ts index e9cf2d9d40d..34ffc227354 100644 --- a/public/app/features/plugins/admin/state/actions.ts +++ b/public/app/features/plugins/admin/state/actions.ts @@ -3,7 +3,6 @@ import { from, forkJoin, timeout, lastValueFrom, catchError, of } from 'rxjs'; import { PanelPlugin, PluginError } from '@grafana/data'; import { config, getBackendSrv, isFetchError } from '@grafana/runtime'; -import { Settings } from 'app/core/config'; import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin'; import { StoreState, ThunkResult } from 'app/types/store'; @@ -301,7 +300,7 @@ export const loadPanelPlugin = (id: string): ThunkResult> = function updatePanels() { return getBackendSrv() .get('/api/frontend/settings') - .then((settings: Settings) => { + .then((settings) => { config.panels = settings.panels; }); } diff --git a/public/app/features/profile/routes.tsx b/public/app/features/profile/routes.tsx index a473b05050c..46ba36dc658 100644 --- a/public/app/features/profile/routes.tsx +++ b/public/app/features/profile/routes.tsx @@ -1,7 +1,7 @@ import { uniq } from 'lodash'; +import { config } from '@grafana/runtime'; import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; -import { config } from 'app/core/config'; import { RouteDescriptor } from 'app/core/navigation/types'; const profileRoutes: RouteDescriptor[] = [ diff --git a/public/app/features/users/UsersActionBar.test.tsx b/public/app/features/users/UsersActionBar.test.tsx index ec58494b21e..83681aac2a0 100644 --- a/public/app/features/users/UsersActionBar.test.tsx +++ b/public/app/features/users/UsersActionBar.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import { mockToolkitActionCreator } from 'test/core/redux/mocks'; -import { config } from 'app/core/config'; +import { config } from '@grafana/runtime'; import { Props, UsersActionBarUnconnected } from './UsersActionBar'; import { searchQueryChanged } from './state/reducers'; diff --git a/public/app/plugins/datasource/alertmanager/ConfigEditor.tsx b/public/app/plugins/datasource/alertmanager/ConfigEditor.tsx index 24bc89e9b90..be7cc9a197a 100644 --- a/public/app/plugins/datasource/alertmanager/ConfigEditor.tsx +++ b/public/app/plugins/datasource/alertmanager/ConfigEditor.tsx @@ -4,8 +4,8 @@ import { Link } from 'react-router-dom-v5-compat'; import { SIGV4ConnectionConfig } from '@grafana/aws-sdk'; import { DataSourcePluginOptionsEditorProps, SelectableValue } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { Box, DataSourceHttpSettings, InlineField, InlineSwitch, Select, Text } from '@grafana/ui'; -import { config } from 'app/core/config'; import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from './types'; diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx index 7fe6b348846..8d585476cc9 100644 --- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx @@ -5,7 +5,7 @@ import { useEffectOnce, useToggle } from 'react-use'; import { GrafanaTheme2, PanelProps } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { TimeRangeUpdatedEvent } from '@grafana/runtime'; +import { config, TimeRangeUpdatedEvent } from '@grafana/runtime'; import { Alert, BigValue, @@ -17,7 +17,6 @@ import { ScrollContainer, useStyles2, } from '@grafana/ui'; -import { config } from 'app/core/config'; import alertDef from 'app/features/alerting/state/alertDef'; import { alertRuleApi } from 'app/features/alerting/unified/api/alertRuleApi'; import { INSTANCES_DISPLAY_LIMIT } from 'app/features/alerting/unified/components/rules/RuleDetails'; diff --git a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx index d8714fd8f4a..2a1ff46fe12 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx @@ -12,10 +12,10 @@ import { PanelProps, VizOrientation, } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { BarGaugeSizing } from '@grafana/schema'; import { BarGauge, DataLinksContextMenu, VizLayout, VizRepeater, VizRepeaterRenderValueProps } from '@grafana/ui'; import { DataLinksContextMenuApi } from '@grafana/ui/internal'; -import { config } from 'app/core/config'; import { BarGaugeLegend } from './BarGaugeLegend'; import { defaultOptions, Options } from './panelcfg.gen'; diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index 4f5f7a46b23..1772f8b2d77 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -5,7 +5,7 @@ import { useMemo, useState } from 'react'; import uPlot from 'uplot'; import { Field, getDisplayProcessor, PanelProps, useDataLinksContext } from '@grafana/data'; -import { PanelDataErrorView } from '@grafana/runtime'; +import { config, PanelDataErrorView } from '@grafana/runtime'; import { DashboardCursorSync, TooltipDisplayMode } from '@grafana/schema'; import { EventBusPlugin, @@ -18,7 +18,6 @@ import { } from '@grafana/ui'; import { AxisProps, ScaleProps, TimeRange2, TooltipHoverMode } from '@grafana/ui/internal'; import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; -import { config } from 'app/core/config'; import { TimeSeriesTooltip } from '../timeseries/TimeSeriesTooltip'; import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2'; diff --git a/public/app/plugins/panel/canvas/components/CanvasContextMenu.tsx b/public/app/plugins/panel/canvas/components/CanvasContextMenu.tsx index be667771d56..8dfd1c48a68 100644 --- a/public/app/plugins/panel/canvas/components/CanvasContextMenu.tsx +++ b/public/app/plugins/panel/canvas/components/CanvasContextMenu.tsx @@ -5,8 +5,8 @@ import { first } from 'rxjs/operators'; import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; import { ContextMenu, MenuItem, MenuItemProps } from '@grafana/ui'; -import { config } from 'app/core/config'; import { ElementState } from 'app/features/canvas/runtime/element'; import { FrameState } from 'app/features/canvas/runtime/frame'; import { Scene } from 'app/features/canvas/runtime/scene'; diff --git a/public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx b/public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx index 3dfcad29220..93713b261a2 100644 --- a/public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx +++ b/public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx @@ -2,9 +2,9 @@ import { css } from '@emotion/css'; import { useEffect, useMemo, useRef, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { DirectionDimensionConfig, DirectionDimensionMode, ConnectionDirection } from '@grafana/schema'; import { useStyles2 } from '@grafana/ui'; -import { config } from 'app/core/config'; import { Scene } from 'app/features/canvas/runtime/scene'; import { ConnectionCoordinates } from '../../panelcfg.gen'; diff --git a/public/app/plugins/panel/canvas/components/connections/ConnectionSVG2.tsx b/public/app/plugins/panel/canvas/components/connections/ConnectionSVG2.tsx index 6e6093250df..8d0219f09b1 100644 --- a/public/app/plugins/panel/canvas/components/connections/ConnectionSVG2.tsx +++ b/public/app/plugins/panel/canvas/components/connections/ConnectionSVG2.tsx @@ -2,9 +2,9 @@ import { css } from '@emotion/css'; import { useEffect, useMemo, useRef, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { DirectionDimensionConfig, DirectionDimensionMode, ConnectionDirection } from '@grafana/schema'; import { useStyles2 } from '@grafana/ui'; -import { config } from 'app/core/config'; import { Scene } from 'app/features/canvas/runtime/scene'; import { ConnectionCoordinates } from '../../panelcfg.gen'; diff --git a/public/app/plugins/panel/canvas/utils.ts b/public/app/plugins/panel/canvas/utils.ts index 59953e9085a..14f42ad9b7e 100644 --- a/public/app/plugins/panel/canvas/utils.ts +++ b/public/app/plugins/panel/canvas/utils.ts @@ -1,9 +1,10 @@ import { isNumber, isString } from 'lodash'; import { DataFrame, Field, AppEvents, getFieldDisplayName, PluginState, SelectableValue } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { ConnectionDirection } from '@grafana/schema'; import { appEvents } from 'app/core/app_events'; -import { hasAlphaPanels, config } from 'app/core/config'; +import { hasAlphaPanels } from 'app/core/config'; import { CanvasConnection, CanvasElementItem, CanvasElementOptions } from 'app/features/canvas/element'; import { notFoundItem } from 'app/features/canvas/elements/notFound'; import { advancedElementItems, canvasElementRegistry, defaultElementItems } from 'app/features/canvas/registry'; diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 3ae1988e30a..0dd53f2505b 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -1,10 +1,10 @@ import { PureComponent, type JSX } from 'react'; import { FieldDisplay, getDisplayProcessor, getFieldDisplayValues, PanelProps } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { BarGaugeSizing, VizOrientation } from '@grafana/schema'; import { DataLinksContextMenu, Gauge, VizRepeater, VizRepeaterRenderValueProps } from '@grafana/ui'; import { DataLinksContextMenuApi } from '@grafana/ui/internal'; -import { config } from 'app/core/config'; import { clearNameForSingleSeries } from '../bargauge/BarGaugePanel'; diff --git a/public/app/plugins/panel/geomap/components/DebugOverlay.tsx b/public/app/plugins/panel/geomap/components/DebugOverlay.tsx index d08d2042f05..9ce3b8f09db 100644 --- a/public/app/plugins/panel/geomap/components/DebugOverlay.tsx +++ b/public/app/plugins/panel/geomap/components/DebugOverlay.tsx @@ -8,7 +8,7 @@ import tinycolor from 'tinycolor2'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans } from '@grafana/i18n'; -import { config } from 'app/core/config'; +import { config } from '@grafana/runtime'; interface Props { map: Map; diff --git a/public/app/plugins/panel/geomap/components/MarkersLegend.tsx b/public/app/plugins/panel/geomap/components/MarkersLegend.tsx index 75d9cfe154b..331ceafe3c3 100644 --- a/public/app/plugins/panel/geomap/components/MarkersLegend.tsx +++ b/public/app/plugins/panel/geomap/components/MarkersLegend.tsx @@ -12,11 +12,11 @@ import { GrafanaTheme2, } from '@grafana/data'; import { t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; import { useStyles2, VizLegendItem } from '@grafana/ui'; import { ColorScale } from 'app/core/components/ColorScale/ColorScale'; import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG'; import { getThresholdItems } from 'app/core/components/TimelineChart/utils'; -import { config } from 'app/core/config'; import { DimensionSupplier } from 'app/features/dimensions/types'; import { StyleConfigState } from '../style/types'; diff --git a/public/app/plugins/panel/geomap/components/MeasureOverlay.tsx b/public/app/plugins/panel/geomap/components/MeasureOverlay.tsx index 90b29f160e9..e6c4b2968eb 100644 --- a/public/app/plugins/panel/geomap/components/MeasureOverlay.tsx +++ b/public/app/plugins/panel/geomap/components/MeasureOverlay.tsx @@ -5,8 +5,8 @@ import { useMemo, useRef, useState } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; import { Button, IconButton, RadioButtonGroup, Select } from '@grafana/ui'; -import { config } from 'app/core/config'; import { MapMeasure, MapMeasureOptions, measures } from '../utils/measure'; diff --git a/public/app/plugins/panel/geomap/layers/registry.ts b/public/app/plugins/panel/geomap/layers/registry.ts index 15e6fbb7861..960da8d7aac 100644 --- a/public/app/plugins/panel/geomap/layers/registry.ts +++ b/public/app/plugins/panel/geomap/layers/registry.ts @@ -9,7 +9,8 @@ import { SelectableValue, PluginState, } from '@grafana/data'; -import { config, hasAlphaPanels } from 'app/core/config'; +import { config } from '@grafana/runtime'; +import { hasAlphaPanels } from 'app/core/config'; import { basemapLayers } from './basemaps'; import { carto } from './basemaps/carto'; diff --git a/public/app/plugins/panel/live/LiveChannelEditor.tsx b/public/app/plugins/panel/live/LiveChannelEditor.tsx index 06af567317e..82f2444ca56 100644 --- a/public/app/plugins/panel/live/LiveChannelEditor.tsx +++ b/public/app/plugins/panel/live/LiveChannelEditor.tsx @@ -10,8 +10,8 @@ import { parseLiveChannelAddress, } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; import { Select, Alert, Label, stylesFactory, Combobox } from '@grafana/ui'; -import { config } from 'app/core/config'; import { discoveryResources, getAPIGroupDiscoveryList, GroupDiscoveryResource } from 'app/features/apiserver/discovery'; import { getManagedChannelInfo } from 'app/features/live/info'; diff --git a/public/app/plugins/panel/radialbar/RadialBarPanel.tsx b/public/app/plugins/panel/radialbar/RadialBarPanel.tsx index 42c70511977..8b32e651f84 100644 --- a/public/app/plugins/panel/radialbar/RadialBarPanel.tsx +++ b/public/app/plugins/panel/radialbar/RadialBarPanel.tsx @@ -7,10 +7,9 @@ import { getFieldDisplayValues, PanelProps, } from '@grafana/data'; -import { PanelDataErrorView } from '@grafana/runtime'; +import { config, PanelDataErrorView } from '@grafana/runtime'; import { DataLinksContextMenu, Stack, VizRepeater, VizRepeaterRenderValueProps } from '@grafana/ui'; import { DataLinksContextMenuApi, RadialGauge } from '@grafana/ui/internal'; -import { config } from 'app/core/config'; import { Options } from './panelcfg.gen'; diff --git a/public/app/plugins/panel/table/suggestions.ts b/public/app/plugins/panel/table/suggestions.ts index 260e73b43eb..bd172f26ad8 100644 --- a/public/app/plugins/panel/table/suggestions.ts +++ b/public/app/plugins/panel/table/suggestions.ts @@ -1,5 +1,5 @@ import { PanelDataSummary, VisualizationSuggestionScore, VisualizationSuggestionsSupplier } from '@grafana/data'; -import { config } from 'app/core/config'; +import { config } from '@grafana/runtime'; import icnTablePanelSvg from 'app/plugins/panel/table/img/icn-table-panel.svg'; import { Options, FieldConfig } from './panelcfg.gen'; diff --git a/public/app/plugins/panel/text/module.tsx b/public/app/plugins/panel/text/module.tsx index dff26482092..ded19ac3185 100644 --- a/public/app/plugins/panel/text/module.tsx +++ b/public/app/plugins/panel/text/module.tsx @@ -1,6 +1,6 @@ import { PanelPlugin } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { config } from 'app/core/config'; +import { config } from '@grafana/runtime'; import { TextPanel } from './TextPanel'; import { TextPanelEditor } from './TextPanelEditor'; diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index eb3b226a248..a85ef58cdc7 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -10,7 +10,7 @@ import { useDataLinksContext, FieldType, } from '@grafana/data'; -import { PanelDataErrorView } from '@grafana/runtime'; +import { config, PanelDataErrorView } from '@grafana/runtime'; import { TooltipDisplayMode, VizOrientation } from '@grafana/schema'; import { EventBusPlugin, @@ -21,7 +21,6 @@ import { } from '@grafana/ui'; import { FILTER_OUT_OPERATOR, TimeRange2, TooltipHoverMode } from '@grafana/ui/internal'; import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; -import { config } from 'app/core/config'; import { TimeSeriesTooltip } from './TimeSeriesTooltip'; import { Options } from './panelcfg.gen'; From 250ca7985ff8567ce314f34220ea331448ef0da9 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Tue, 13 Jan 2026 08:25:40 +0200 Subject: [PATCH 09/14] Provisioning: Add Connections page (#116060) * Provisioning: Add connections page * Provisioning: Add connections form * Provisioning: Add connections form * Update fields * Fix generated name * Update connection name * Add edit page * error handling * Form validation * Add Connections button * Cleanup * Extract ConnectionFormData type * Add list test and separate empty states * Add form test * Update tests * i18n * Cleanup * Use SecretTextArea from grafana-ui * Fix breadcrumbs * tweaks * Add missing URL * Switch to ShowConfirmModalEvent * i18n * redirect to list on success * add timeout * Fix tags invalidation --- .../SecretTextArea/SecretTextArea.tsx | 6 +- .../clients/provisioning/v0alpha1/index.ts | 125 ++++++-- .../provisioning/Config/ConfigForm.tsx | 19 +- .../Connection/ConnectionForm.test.tsx | 277 ++++++++++++++++++ .../Connection/ConnectionForm.tsx | 199 +++++++++++++ .../Connection/ConnectionFormPage.tsx | 59 ++++ .../Connection/ConnectionList.test.tsx | 165 +++++++++++ .../Connection/ConnectionList.tsx | 51 ++++ .../Connection/ConnectionListItem.tsx | 49 ++++ .../Connection/ConnectionStatusBadge.tsx | 42 +++ .../Connection/ConnectionsPage.tsx | 55 ++++ .../Connection/DeleteConnectionButton.tsx | 53 ++++ .../Repository/DeleteRepositoryButton.tsx | 182 ++++++------ .../Repository/RepositoryActions.tsx | 5 +- public/app/features/provisioning/constants.ts | 1 + .../provisioning/hooks/useConnectionList.ts | 17 ++ .../hooks/useCreateOrUpdateConnection.ts | 40 +++ public/app/features/provisioning/types.ts | 11 + .../provisioning/utils/getFormErrors.ts | 19 +- .../app/features/provisioning/utils/routes.ts | 22 +- public/locales/en-US/grafana.json | 47 +++ 21 files changed, 1301 insertions(+), 143 deletions(-) create mode 100644 public/app/features/provisioning/Connection/ConnectionForm.test.tsx create mode 100644 public/app/features/provisioning/Connection/ConnectionForm.tsx create mode 100644 public/app/features/provisioning/Connection/ConnectionFormPage.tsx create mode 100644 public/app/features/provisioning/Connection/ConnectionList.test.tsx create mode 100644 public/app/features/provisioning/Connection/ConnectionList.tsx create mode 100644 public/app/features/provisioning/Connection/ConnectionListItem.tsx create mode 100644 public/app/features/provisioning/Connection/ConnectionStatusBadge.tsx create mode 100644 public/app/features/provisioning/Connection/ConnectionsPage.tsx create mode 100644 public/app/features/provisioning/Connection/DeleteConnectionButton.tsx create mode 100644 public/app/features/provisioning/hooks/useConnectionList.ts create mode 100644 public/app/features/provisioning/hooks/useCreateOrUpdateConnection.ts diff --git a/packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.tsx b/packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.tsx index a10ad157120..5b919c05ec2 100644 --- a/packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.tsx +++ b/packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.tsx @@ -14,6 +14,8 @@ export type Props = React.ComponentProps & { isConfigured: boolean; /** Called when the user clicks on the "Reset" button in order to clear the secret */ onReset: () => void; + /** If true, the text area will grow to fill available width. */ + grow?: boolean; }; export const CONFIGURED_TEXT = 'configured'; @@ -35,11 +37,11 @@ const getStyles = (theme: GrafanaTheme2) => { * * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-secrettextarea--docs */ -export const SecretTextArea = ({ isConfigured, onReset, ...props }: Props) => { +export const SecretTextArea = ({ isConfigured, onReset, grow, ...props }: Props) => { const styles = useStyles2(getStyles); return ( - + {!isConfigured &&