From bcb2a7e36f7ae5190ab45669f4a0d04660313842 Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Tue, 6 May 2025 06:55:23 +0200 Subject: [PATCH] Plugin Extensions: Read-only props for extension components (#102079) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: pass read-only props to extension components This initial commit is used to verify that things are broken in case the datasource object is not cloned before passed in as a prop. * chore: update tests --------- Co-authored-by: Hugo Häggmark --- .../src/types/featureToggles.gen.ts | 4 + pkg/services/featuremgmt/registry.go | 9 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 636 ++++++++++++------ .../datasources/components/EditDataSource.tsx | 5 +- .../features/plugins/extensions/logs/log.ts | 7 +- .../extensions/usePluginComponent.test.tsx | 139 +++- .../extensions/usePluginComponents.test.tsx | 158 ++++- .../extensions/usePluginComponents.tsx | 4 +- .../plugins/extensions/utils.test.tsx | 314 ++++++++- .../app/features/plugins/extensions/utils.tsx | 108 ++- 12 files changed, 1122 insertions(+), 267 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 9e3522b32b6..e529447577c 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1026,4 +1026,8 @@ export interface FeatureToggles { * @default true */ alertingBulkActionsInUI?: boolean; + /** + * Use proxy-based read-only objects for plugin extensions instead of deep cloning + */ + extensionsReadOnlyProxy?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index d7da1d700b2..a134deb2095 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1765,6 +1765,15 @@ var ( HideFromDocs: true, Expression: "true", // enabled by default }, + { + Name: "extensionsReadOnlyProxy", + Description: "Use proxy-based read-only objects for plugin extensions instead of deep cloning", + Stage: FeatureStageExperimental, + Owner: grafanaPluginsPlatformSquad, + HideFromAdminPage: true, + HideFromDocs: true, + FrontendOnly: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index b6cb09cc365..8aa14d6a79c 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -231,3 +231,4 @@ multiTenantFrontend,experimental,@grafana/grafana-frontend-platform,false,false, alertingListViewV2PreviewToggle,privatePreview,@grafana/alerting-squad,false,false,true alertRuleUseFiredAtForStartsAt,experimental,@grafana/alerting-squad,false,false,false alertingBulkActionsInUI,GA,@grafana/alerting-squad,false,false,true +extensionsReadOnlyProxy,experimental,@grafana/plugins-platform-backend,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index ba2dd4d0781..59a051af759 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -934,4 +934,8 @@ const ( // FlagAlertingBulkActionsInUI // Enables the alerting bulk actions in the UI FlagAlertingBulkActionsInUI = "alertingBulkActionsInUI" + + // FlagExtensionsReadOnlyProxy + // Use proxy-based read-only objects for plugin extensions instead of deep cloning + FlagExtensionsReadOnlyProxy = "extensionsReadOnlyProxy" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 357a2e828e5..974a29d2301 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -174,7 +174,7 @@ { "metadata": { "name": "alertingJiraIntegration", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-02-14T12:22:04Z" }, "spec": { @@ -188,7 +188,7 @@ { "metadata": { "name": "alertingListViewV2", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-05-24T14:40:49Z" }, "spec": { @@ -214,7 +214,7 @@ { "metadata": { "name": "alertingMigrationUI", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-03-14T16:40:05Z" }, "spec": { @@ -229,7 +229,7 @@ { "metadata": { "name": "alertingNotificationsStepMode", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-11-22T11:07:45Z" }, "spec": { @@ -243,7 +243,7 @@ { "metadata": { "name": "alertingPrometheusRulesPrimary", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-09-27T12:27:16Z" }, "spec": { @@ -256,7 +256,7 @@ { "metadata": { "name": "alertingQueryAndExpressionsStepMode", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-09-26T06:33:14Z" }, "spec": { @@ -270,7 +270,7 @@ { "metadata": { "name": "alertingQueryOptimization", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-01-10T20:52:58Z" }, "spec": { @@ -283,7 +283,7 @@ { "metadata": { "name": "alertingRulePermanentlyDelete", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-04-03T11:18:25Z" }, "spec": { @@ -299,7 +299,7 @@ { "metadata": { "name": "alertingRuleRecoverDeleted", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-03-27T14:39:26Z" }, "spec": { @@ -315,7 +315,7 @@ { "metadata": { "name": "alertingRuleVersionHistoryRestore", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-02-17T12:25:32Z" }, "spec": { @@ -331,7 +331,7 @@ { "metadata": { "name": "alertingSaveStateCompressed", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-01-27T17:47:33Z" }, "spec": { @@ -344,7 +344,7 @@ { "metadata": { "name": "alertingSaveStatePeriodic", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-01-23T16:03:30Z" }, "spec": { @@ -356,7 +356,7 @@ { "metadata": { "name": "alertingSimplifiedRouting", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-11-10T13:14:39Z" }, "spec": { @@ -369,7 +369,7 @@ { "metadata": { "name": "alertingUIOptimizeReducer", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-11-18T10:59:00Z" }, "spec": { @@ -383,7 +383,7 @@ { "metadata": { "name": "alertmanagerRemoteOnly", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-10-30T16:27:08Z" }, "spec": { @@ -395,7 +395,7 @@ { "metadata": { "name": "alertmanagerRemotePrimary", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-10-30T16:27:08Z" }, "spec": { @@ -407,7 +407,7 @@ { "metadata": { "name": "alertmanagerRemoteSecondary", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-10-30T16:27:08Z" }, "spec": { @@ -419,7 +419,7 @@ { "metadata": { "name": "angularDeprecationUI", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-08-29T14:05:47Z" }, "spec": { @@ -433,7 +433,7 @@ { "metadata": { "name": "annotationPermissionUpdate", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-10-31T13:30:13Z" }, "spec": { @@ -446,7 +446,7 @@ { "metadata": { "name": "appPlatformGrpcClientAuth", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-10-14T10:47:18Z" }, "spec": { @@ -457,10 +457,23 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "appSidecar", + "resourceVersion": "1743693517832", + "creationTimestamp": "2024-09-09T12:45:05Z", + "deletionTimestamp": "2025-04-10T20:04:12Z" + }, + "spec": { + "description": "Enable the app sidecar feature that allows rendering 2 apps at the same time", + "stage": "experimental", + "codeowner": "@grafana/grafana-frontend-platform" + } + }, { "metadata": { "name": "assetSriChecks", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-03-04T10:56:35Z" }, "spec": { @@ -473,7 +486,7 @@ { "metadata": { "name": "authZGRPCServer", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-06-13T09:41:35Z" }, "spec": { @@ -487,7 +500,7 @@ { "metadata": { "name": "awsAsyncQueryCaching", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-07-21T15:34:07Z" }, "spec": { @@ -500,7 +513,7 @@ { "metadata": { "name": "awsDatasourcesTempCredentials", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-07-06T15:06:11Z" }, "spec": { @@ -512,7 +525,7 @@ { "metadata": { "name": "azureMonitorDisableLogLimit", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-10-24T13:32:09Z" }, "spec": { @@ -525,7 +538,7 @@ { "metadata": { "name": "azureMonitorEnableUserAuth", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-11-27T14:01:54Z" }, "spec": { @@ -538,7 +551,7 @@ { "metadata": { "name": "azureMonitorLogsBuilderEditor", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-04-02T14:15:25Z" }, "spec": { @@ -551,7 +564,7 @@ { "metadata": { "name": "azureMonitorPrometheusExemplars", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-06-06T16:53:17Z" }, "spec": { @@ -564,7 +577,7 @@ { "metadata": { "name": "cachingOptimizeSerializationMemoryUsage", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-10-12T16:56:49Z" }, "spec": { @@ -576,7 +589,7 @@ { "metadata": { "name": "canvasPanelNesting", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2022-05-31T19:03:34Z" }, "spec": { @@ -590,7 +603,7 @@ { "metadata": { "name": "canvasPanelPanZoom", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-01-02T19:52:21Z" }, "spec": { @@ -603,7 +616,7 @@ { "metadata": { "name": "cloudRBACRoles", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-01-10T13:19:01Z" }, "spec": { @@ -619,7 +632,7 @@ { "metadata": { "name": "cloudWatchBatchQueries", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-10-20T19:09:41Z" }, "spec": { @@ -631,7 +644,7 @@ { "metadata": { "name": "cloudWatchCrossAccountQuerying", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2022-11-28T11:39:12Z" }, "spec": { @@ -645,7 +658,7 @@ { "metadata": { "name": "cloudWatchNewLabelParsing", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-04-05T15:57:56Z" }, "spec": { @@ -658,7 +671,7 @@ { "metadata": { "name": "cloudWatchRoundUpEndTime", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-06-27T15:10:28Z" }, "spec": { @@ -671,7 +684,7 @@ { "metadata": { "name": "configurableSchedulerTick", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-07-26T16:44:12Z" }, "spec": { @@ -685,7 +698,7 @@ { "metadata": { "name": "correlations", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2022-09-16T13:14:27Z" }, "spec": { @@ -699,7 +712,7 @@ { "metadata": { "name": "crashDetection", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-11-12T15:07:27Z" }, "spec": { @@ -736,7 +749,7 @@ { "metadata": { "name": "dashboardNewLayouts", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-10-23T08:55:45Z" }, "spec": { @@ -749,7 +762,7 @@ { "metadata": { "name": "dashboardScene", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-11-13T08:51:21Z" }, "spec": { @@ -763,7 +776,7 @@ { "metadata": { "name": "dashboardSceneForViewers", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-11-02T19:02:25Z" }, "spec": { @@ -777,7 +790,7 @@ { "metadata": { "name": "dashboardSceneSolo", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-02-11T08:08:47Z" }, "spec": { @@ -803,7 +816,7 @@ { "metadata": { "name": "dashgpt", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-08-30T20:22:05Z" }, "spec": { @@ -817,7 +830,7 @@ { "metadata": { "name": "dataplaneAggregator", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-08-09T08:41:07Z" }, "spec": { @@ -830,7 +843,7 @@ { "metadata": { "name": "dataplaneFrontendFallback", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-04-07T21:13:19Z" }, "spec": { @@ -845,7 +858,7 @@ { "metadata": { "name": "datasourceAPIServers", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-09-19T08:28:27Z" }, "spec": { @@ -858,7 +871,7 @@ { "metadata": { "name": "datasourceConnectionsTab", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-01-21T17:39:48Z" }, "spec": { @@ -871,7 +884,7 @@ { "metadata": { "name": "datasourceQueryTypes", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-05-23T16:46:28Z" }, "spec": { @@ -884,7 +897,7 @@ { "metadata": { "name": "disableClassicHTTPHistogram", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-06-18T19:37:44Z" }, "spec": { @@ -898,7 +911,7 @@ { "metadata": { "name": "disableEnvelopeEncryption", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2022-05-24T08:34:47Z" }, "spec": { @@ -912,7 +925,7 @@ { "metadata": { "name": "disableNumericMetricsSortingInExpressions", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-04-16T14:52:47Z" }, "spec": { @@ -925,7 +938,7 @@ { "metadata": { "name": "disableSSEDataplane", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-04-12T16:24:34Z" }, "spec": { @@ -934,10 +947,24 @@ "codeowner": "@grafana/observability-metrics" } }, + { + "metadata": { + "name": "disableSecretsCompatibility", + "resourceVersion": "1743693517832", + "creationTimestamp": "2022-07-12T20:27:37Z", + "deletionTimestamp": "2025-05-05T08:21:21Z" + }, + "spec": { + "description": "Disable duplicated secret storage in legacy tables", + "stage": "experimental", + "codeowner": "@grafana/hosted-grafana-team", + "requiresRestart": true + } + }, { "metadata": { "name": "editPanelCSVDragAndDrop", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-01-24T09:43:44Z" }, "spec": { @@ -950,7 +977,7 @@ { "metadata": { "name": "elasticsearchCrossClusterSearch", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-12-12T22:20:04Z" }, "spec": { @@ -962,7 +989,7 @@ { "metadata": { "name": "elasticsearchImprovedParsing", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-01-15T17:05:54Z" }, "spec": { @@ -974,7 +1001,7 @@ { "metadata": { "name": "enableDatagridEditing", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-04-24T14:46:31Z" }, "spec": { @@ -987,7 +1014,7 @@ { "metadata": { "name": "enableExtensionsAdminPage", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-11-05T15:55:10Z" }, "spec": { @@ -1000,7 +1027,7 @@ { "metadata": { "name": "enableNativeHTTPHistogram", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-10-03T18:23:55Z" }, "spec": { @@ -1014,7 +1041,7 @@ { "metadata": { "name": "enableSCIM", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-11-07T14:38:46Z" }, "spec": { @@ -1026,7 +1053,7 @@ { "metadata": { "name": "enableScopesInMetricsExplore", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-11-06T13:11:33Z" }, "spec": { @@ -1040,7 +1067,7 @@ { "metadata": { "name": "exploreLogsAggregatedMetrics", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { @@ -1053,7 +1080,7 @@ { "metadata": { "name": "exploreLogsLimitedTimeRange", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { @@ -1066,7 +1093,7 @@ { "metadata": { "name": "exploreLogsShardSplitting", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { @@ -1076,10 +1103,25 @@ "frontend": true } }, + { + "metadata": { + "name": "exploreMetrics", + "resourceVersion": "1743693517832", + "creationTimestamp": "2024-04-09T18:15:18Z", + "deletionTimestamp": "2025-04-11T20:45:14Z" + }, + "spec": { + "description": "Enables the new Grafana Metrics Drilldown core app", + "stage": "GA", + "codeowner": "@grafana/observability-metrics", + "frontend": true, + "expression": "true" + } + }, { "metadata": { "name": "exploreMetricsRelatedLogs", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-11-05T16:28:43Z" }, "spec": { @@ -1089,10 +1131,24 @@ "frontend": true } }, + { + "metadata": { + "name": "exploreMetricsUseExternalAppPlugin", + "resourceVersion": "1743693517832", + "creationTimestamp": "2025-02-03T20:46:54Z", + "deletionTimestamp": "2025-04-11T20:45:14Z" + }, + "spec": { + "description": "Use the externalized Grafana Metrics Drilldown (formerly known as Explore Metrics) app plugin", + "stage": "preview", + "codeowner": "@grafana/observability-metrics", + "requiresRestart": true + } + }, { "metadata": { "name": "expressionParser", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-02-17T00:59:11Z" }, "spec": { @@ -1105,7 +1161,7 @@ { "metadata": { "name": "extensionSidebar", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-04-03T10:16:35Z" }, "spec": { @@ -1115,10 +1171,25 @@ "frontend": true } }, + { + "metadata": { + "name": "extensionsReadOnlyProxy", + "resourceVersion": "1744122618262", + "creationTimestamp": "2025-04-08T14:30:18Z" + }, + "spec": { + "description": "Use proxy-based read-only objects for plugin extensions instead of deep cloning", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend", + "frontend": true, + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "externalCorePlugins", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-09-22T08:50:13Z" }, "spec": { @@ -1131,7 +1202,7 @@ { "metadata": { "name": "externalServiceAccounts", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-09-28T07:26:37Z" }, "spec": { @@ -1144,7 +1215,7 @@ { "metadata": { "name": "extraThemes", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-05-10T13:37:04Z" }, "spec": { @@ -1157,7 +1228,7 @@ { "metadata": { "name": "extractFieldsNameDeduplication", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-11-02T15:47:42Z" }, "spec": { @@ -1170,7 +1241,7 @@ { "metadata": { "name": "failWrongDSUID", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-06-20T10:56:39Z" }, "spec": { @@ -1183,7 +1254,7 @@ { "metadata": { "name": "faroDatasourceSelector", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-05-05T00:35:10Z" }, "spec": { @@ -1196,7 +1267,7 @@ { "metadata": { "name": "featureHighlights", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2022-02-03T11:53:23Z" }, "spec": { @@ -1210,7 +1281,7 @@ { "metadata": { "name": "featureToggleAdminPage", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-07-18T20:43:32Z" }, "spec": { @@ -1224,7 +1295,7 @@ { "metadata": { "name": "feedbackButton", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-12-02T17:08:15Z" }, "spec": { @@ -1237,7 +1308,7 @@ { "metadata": { "name": "fetchRulesUsingPost", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-01-29T12:17:44Z" }, "spec": { @@ -1251,7 +1322,7 @@ { "metadata": { "name": "formatString", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-10-13T18:17:12Z" }, "spec": { @@ -1262,10 +1333,24 @@ "expression": "true" } }, + { + "metadata": { + "name": "frontendSandboxMonitorOnly", + "resourceVersion": "1743693517832", + "creationTimestamp": "2023-07-05T11:48:25Z", + "deletionTimestamp": "2025-05-05T08:21:21Z" + }, + "spec": { + "description": "Enables monitor only in the plugin frontend sandbox (if enabled)", + "stage": "privatePreview", + "codeowner": "@grafana/plugins-platform-backend", + "frontend": true + } + }, { "metadata": { "name": "grafanaAPIServerEnsureKubectlAccess", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-12-06T20:21:21Z" }, "spec": { @@ -1279,7 +1364,7 @@ { "metadata": { "name": "grafanaAPIServerWithExperimentalAPIs", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-10-06T18:55:22Z" }, "spec": { @@ -1293,7 +1378,7 @@ { "metadata": { "name": "grafanaAdvisor", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-01-20T10:08:00Z" }, "spec": { @@ -1305,7 +1390,7 @@ { "metadata": { "name": "grafanaManagedRecordingRules", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-04-22T17:53:16Z" }, "spec": { @@ -1319,7 +1404,7 @@ { "metadata": { "name": "grafanaManagedRecordingRulesDatasources", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-03-07T13:30:40Z" }, "spec": { @@ -1333,7 +1418,7 @@ { "metadata": { "name": "grafanaconThemes", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-02-06T11:08:04Z" }, "spec": { @@ -1349,7 +1434,7 @@ { "metadata": { "name": "groupAttributeSync", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-09-09T15:29:43Z" }, "spec": { @@ -1362,7 +1447,7 @@ { "metadata": { "name": "groupByVariable", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-02-14T17:18:04Z" }, "spec": { @@ -1376,7 +1461,7 @@ { "metadata": { "name": "groupToNestedTableTransformation", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-02-07T14:28:26Z" }, "spec": { @@ -1390,7 +1475,7 @@ { "metadata": { "name": "grpcServer", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2022-09-26T20:25:34Z" }, "spec": { @@ -1403,7 +1488,7 @@ { "metadata": { "name": "homeSetupGuide", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-09-25T17:20:04Z" }, "spec": { @@ -1416,7 +1501,7 @@ { "metadata": { "name": "improvedExternalSessionHandling", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-09-17T10:54:39Z" }, "spec": { @@ -1429,7 +1514,7 @@ { "metadata": { "name": "improvedExternalSessionHandlingSAML", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-01-09T17:02:49Z" }, "spec": { @@ -1442,7 +1527,7 @@ { "metadata": { "name": "individualCookiePreferences", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-02-21T10:19:07Z" }, "spec": { @@ -1454,7 +1539,7 @@ { "metadata": { "name": "infinityRunQueriesInParallel", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-03-14T12:54:04Z" }, "spec": { @@ -1466,7 +1551,7 @@ { "metadata": { "name": "influxdbBackendMigration", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2022-02-09T18:26:16Z", "deletionTimestamp": "2023-01-17T14:11:26Z" }, @@ -1481,7 +1566,7 @@ { "metadata": { "name": "influxdbRunQueriesInParallel", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-02-01T10:58:24Z" }, "spec": { @@ -1493,7 +1578,7 @@ { "metadata": { "name": "influxqlStreamingParser", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-11-29T17:29:35Z" }, "spec": { @@ -1505,7 +1590,7 @@ { "metadata": { "name": "investigationsBackend", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-12-18T08:31:03Z" }, "spec": { @@ -1518,7 +1603,7 @@ { "metadata": { "name": "inviteUserExperimental", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-03-07T19:09:59Z" }, "spec": { @@ -1533,7 +1618,7 @@ { "metadata": { "name": "jaegerBackendMigration", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-11-15T14:40:20Z" }, "spec": { @@ -1545,7 +1630,7 @@ { "metadata": { "name": "jitterAlertRulesWithinGroups", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-01-18T18:48:11Z" }, "spec": { @@ -1559,7 +1644,7 @@ { "metadata": { "name": "k8SFolderCounts", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-12-27T17:10:44Z" }, "spec": { @@ -1572,7 +1657,7 @@ { "metadata": { "name": "k8SFolderMove", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-12-27T17:10:44Z" }, "spec": { @@ -1585,7 +1670,7 @@ { "metadata": { "name": "kubernetesAggregator", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-02-12T20:59:35Z" }, "spec": { @@ -1598,7 +1683,7 @@ { "metadata": { "name": "kubernetesClientDashboardsFolders", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-02-18T23:11:26Z" }, "spec": { @@ -1611,7 +1696,7 @@ { "metadata": { "name": "kubernetesDashboards", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-06-05T14:34:23Z" }, "spec": { @@ -1624,7 +1709,7 @@ { "metadata": { "name": "kubernetesFeatureToggles", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-01-18T05:32:44Z" }, "spec": { @@ -1635,10 +1720,25 @@ "hideFromAdminPage": true } }, + { + "metadata": { + "name": "kubernetesPlaylists", + "resourceVersion": "1743693517832", + "creationTimestamp": "2023-10-05T19:00:36Z", + "deletionTimestamp": "2024-08-13T08:03:28Z" + }, + "spec": { + "description": "Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s", + "stage": "GA", + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresRestart": true, + "expression": "true" + } + }, { "metadata": { "name": "kubernetesSnapshots", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-12-05T22:31:49Z" }, "spec": { @@ -1651,7 +1751,7 @@ { "metadata": { "name": "libraryPanelRBAC", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-10-11T23:30:50Z" }, "spec": { @@ -1664,7 +1764,7 @@ { "metadata": { "name": "localeFormatPreference", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-03-31T13:59:07Z" }, "spec": { @@ -1676,7 +1776,7 @@ { "metadata": { "name": "localizationForPlugins", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-03-31T04:38:38Z" }, "spec": { @@ -1688,7 +1788,7 @@ { "metadata": { "name": "logQLScope", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-11-11T11:53:24Z" }, "spec": { @@ -1703,7 +1803,7 @@ { "metadata": { "name": "logRequestsInstrumentedAsUnknown", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2022-06-10T08:56:55Z" }, "spec": { @@ -1715,7 +1815,7 @@ { "metadata": { "name": "logRowsPopoverMenu", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-11-16T09:48:10Z" }, "spec": { @@ -1729,7 +1829,7 @@ { "metadata": { "name": "logsContextDatasourceUi", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-01-27T14:12:01Z" }, "spec": { @@ -1744,7 +1844,7 @@ { "metadata": { "name": "logsExploreTableDefaultVisualization", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-05-02T15:28:15Z" }, "spec": { @@ -1757,7 +1857,7 @@ { "metadata": { "name": "logsExploreTableVisualisation", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-07-12T13:52:42Z" }, "spec": { @@ -1771,7 +1871,7 @@ { "metadata": { "name": "logsInfiniteScrolling", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-11-09T10:54:03Z" }, "spec": { @@ -1785,8 +1885,11 @@ { "metadata": { "name": "logsPanelControls", - "resourceVersion": "1745491786560", - "creationTimestamp": "2025-04-07T14:38:55Z" + "resourceVersion": "1743772342343", + "creationTimestamp": "2025-04-07T14:38:55Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-04-04 13:12:22.343052 +0000 UTC" + } }, "spec": { "description": "Enables a control component for the logs panel in Explore", @@ -1799,7 +1902,7 @@ { "metadata": { "name": "lokiExperimentalStreaming", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-06-19T10:03:51Z" }, "spec": { @@ -1811,7 +1914,7 @@ { "metadata": { "name": "lokiLabelNamesQueryApi", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-12-13T14:31:41Z" }, "spec": { @@ -1824,7 +1927,7 @@ { "metadata": { "name": "lokiLogsDataplane", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-07-13T07:58:00Z" }, "spec": { @@ -1836,7 +1939,7 @@ { "metadata": { "name": "lokiPredefinedOperations", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-06-02T10:52:36Z" }, "spec": { @@ -1849,7 +1952,7 @@ { "metadata": { "name": "lokiQueryHints", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-12-18T20:43:16Z" }, "spec": { @@ -1863,7 +1966,7 @@ { "metadata": { "name": "lokiQuerySplitting", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-02-09T17:27:02Z" }, "spec": { @@ -1878,7 +1981,7 @@ { "metadata": { "name": "lokiQuerySplittingConfig", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-03-20T15:51:36Z" }, "spec": { @@ -1891,7 +1994,7 @@ { "metadata": { "name": "lokiRunQueriesInParallel", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-09-19T09:34:01Z" }, "spec": { @@ -1903,7 +2006,7 @@ { "metadata": { "name": "lokiSendDashboardPanelNames", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-08-22T19:30:43Z" }, "spec": { @@ -1915,7 +2018,7 @@ { "metadata": { "name": "lokiShardSplitting", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-10-23T11:21:03Z" }, "spec": { @@ -1928,7 +2031,7 @@ { "metadata": { "name": "lokiStructuredMetadata", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-11-16T16:06:14Z" }, "spec": { @@ -1941,7 +2044,7 @@ { "metadata": { "name": "managedDualWriter", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-02-19T14:50:39Z" }, "spec": { @@ -1968,7 +2071,7 @@ { "metadata": { "name": "mlExpressions", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-07-13T17:37:50Z" }, "spec": { @@ -1992,7 +2095,7 @@ { "metadata": { "name": "multiTenantTempCredentials", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-04-02T20:25:50Z" }, "spec": { @@ -2018,7 +2121,7 @@ { "metadata": { "name": "mysqlAnsiQuotes", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2022-10-12T11:43:35Z" }, "spec": { @@ -2030,7 +2133,7 @@ { "metadata": { "name": "nestedFolders", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2022-10-26T14:15:14Z" }, "spec": { @@ -2043,7 +2146,7 @@ { "metadata": { "name": "newDashboardSharingComponent", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-05-03T15:02:18Z" }, "spec": { @@ -2057,7 +2160,7 @@ { "metadata": { "name": "newDashboardWithFiltersAndGroupBy", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-04-04T11:25:21Z" }, "spec": { @@ -2071,7 +2174,7 @@ { "metadata": { "name": "newFiltersUI", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-08-30T12:48:13Z" }, "spec": { @@ -2084,7 +2187,7 @@ { "metadata": { "name": "newFolderPicker", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-01-15T11:43:19Z" }, "spec": { @@ -2097,7 +2200,7 @@ { "metadata": { "name": "newLogsPanel", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-02-04T17:40:17Z" }, "spec": { @@ -2110,7 +2213,7 @@ { "metadata": { "name": "newPDFRendering", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-02-08T12:09:34Z" }, "spec": { @@ -2123,7 +2226,7 @@ { "metadata": { "name": "newShareReportDrawer", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-02-17T19:05:46Z" }, "spec": { @@ -2134,10 +2237,42 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "noBackdropBlur", + "resourceVersion": "1744057771109", + "creationTimestamp": "2025-03-14T15:21:35Z", + "deletionTimestamp": "2025-04-08T10:58:19Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-04-07 20:29:31.109804663 +0000 UTC" + } + }, + "spec": { + "description": "Disables backdrop blur behind modals and drawers", + "stage": "GA", + "codeowner": "@grafana/grafana-frontend-platform", + "frontend": true, + "expression": "true" + } + }, + { + "metadata": { + "name": "nodeGraphDotLayout", + "resourceVersion": "1743693517832", + "creationTimestamp": "2024-01-31T16:26:12Z", + "deletionTimestamp": "2025-04-08T14:37:17Z" + }, + "spec": { + "description": "Changed the layout algorithm for the node graph", + "stage": "experimental", + "codeowner": "@grafana/observability-traces-and-profiling", + "frontend": true + } + }, { "metadata": { "name": "oauthRequireSubClaim", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-03-25T13:22:24Z" }, "spec": { @@ -2151,7 +2286,7 @@ { "metadata": { "name": "onPremToCloudMigrations", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-01-22T16:09:08Z" }, "spec": { @@ -2164,7 +2299,7 @@ { "metadata": { "name": "panelFilterVariable", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-11-03T12:15:54Z" }, "spec": { @@ -2178,7 +2313,7 @@ { "metadata": { "name": "panelMonitoring", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-10-09T05:19:08Z" }, "spec": { @@ -2192,7 +2327,7 @@ { "metadata": { "name": "panelTitleSearch", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2022-02-15T18:26:03Z" }, "spec": { @@ -2205,7 +2340,7 @@ { "metadata": { "name": "passwordlessMagicLinkAuthentication", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-11-14T13:50:55Z" }, "spec": { @@ -2219,7 +2354,7 @@ { "metadata": { "name": "pdfTables", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-11-06T13:39:22Z" }, "spec": { @@ -2231,7 +2366,7 @@ { "metadata": { "name": "permissionsFilterRemoveSubquery", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-08-02T07:39:25Z" }, "spec": { @@ -2243,7 +2378,7 @@ { "metadata": { "name": "pinNavItems", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-06-10T11:40:03Z" }, "spec": { @@ -2256,7 +2391,7 @@ { "metadata": { "name": "playlistsReconciler", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-12-20T03:09:31Z" }, "spec": { @@ -2269,7 +2404,7 @@ { "metadata": { "name": "pluginProxyPreserveTrailingSlash", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-06-05T11:36:14Z" }, "spec": { @@ -2279,11 +2414,24 @@ "expression": "false" } }, + { + "metadata": { + "name": "pluginsAPIMetrics", + "resourceVersion": "1743693517832", + "creationTimestamp": "2023-09-21T11:36:32Z", + "deletionTimestamp": "2025-05-05T08:21:21Z" + }, + "spec": { + "description": "Enables auto-updating of users installed plugins", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend" + } + }, { "metadata": { "name": "pluginsAutoUpdate", - "resourceVersion": "1745491786560", - "creationTimestamp": "2025-04-24T10:49:46Z" + "resourceVersion": "1746433281774", + "creationTimestamp": "2025-05-05T08:21:21Z" }, "spec": { "description": "Enables auto-updating of users installed plugins", @@ -2294,7 +2442,7 @@ { "metadata": { "name": "pluginsCDNSyncLoader", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-02-07T10:07:08Z" }, "spec": { @@ -2306,7 +2454,7 @@ { "metadata": { "name": "pluginsDetailsRightPanel", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-08-13T09:55:30Z" }, "spec": { @@ -2320,7 +2468,7 @@ { "metadata": { "name": "pluginsFrontendSandbox", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-06-05T08:51:36Z" }, "spec": { @@ -2332,7 +2480,7 @@ { "metadata": { "name": "pluginsSkipHostEnvVars", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-11-15T17:09:14Z" }, "spec": { @@ -2344,7 +2492,7 @@ { "metadata": { "name": "pluginsSriChecks", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-10-04T12:55:09Z" }, "spec": { @@ -2357,7 +2505,7 @@ { "metadata": { "name": "preinstallAutoUpdate", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-11-07T12:14:25Z" }, "spec": { @@ -2370,7 +2518,7 @@ { "metadata": { "name": "preserveDashboardStateWhenNavigating", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-05-27T12:28:06Z" }, "spec": { @@ -2384,7 +2532,7 @@ { "metadata": { "name": "promQLScope", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-01-29T20:22:17Z" }, "spec": { @@ -2399,7 +2547,7 @@ { "metadata": { "name": "prometheusAzureOverrideAudience", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2022-05-30T15:43:32Z", "deletionTimestamp": "2023-07-16T21:30:14Z" }, @@ -2413,7 +2561,7 @@ { "metadata": { "name": "prometheusCodeModeMetricNamesSearch", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-04-04T20:38:23Z" }, "spec": { @@ -2423,10 +2571,24 @@ "frontend": true } }, + { + "metadata": { + "name": "prometheusRunQueriesInParallel", + "resourceVersion": "1743693517832", + "creationTimestamp": "2024-08-12T12:31:39Z", + "deletionTimestamp": "2025-04-11T22:11:19Z" + }, + "spec": { + "description": "Enables running Prometheus queries in parallel", + "stage": "GA", + "codeowner": "@grafana/oss-big-tent", + "expression": "true" + } + }, { "metadata": { "name": "prometheusSpecialCharsInLabelValues", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-12-18T21:31:08Z" }, "spec": { @@ -2436,10 +2598,24 @@ "frontend": true } }, + { + "metadata": { + "name": "prometheusUsesCombobox", + "resourceVersion": "1743693517832", + "creationTimestamp": "2024-10-23T11:18:33Z", + "deletionTimestamp": "2025-05-05T08:21:21Z" + }, + "spec": { + "description": "Use new **Combobox** component for Prometheus query editor", + "stage": "GA", + "codeowner": "@grafana/oss-big-tent", + "expression": "true" + } + }, { "metadata": { "name": "provisioning", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-11-22T09:03:50Z" }, "spec": { @@ -2452,7 +2628,7 @@ { "metadata": { "name": "publicDashboardsEmailSharing", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-01-03T19:45:15Z" }, "spec": { @@ -2466,7 +2642,7 @@ { "metadata": { "name": "publicDashboardsScene", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-03-22T14:48:21Z" }, "spec": { @@ -2480,7 +2656,7 @@ { "metadata": { "name": "queryLibrary", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2022-10-07T18:31:45Z", "deletionTimestamp": "2023-03-20T16:00:14Z" }, @@ -2493,7 +2669,7 @@ { "metadata": { "name": "queryService", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -2506,7 +2682,7 @@ { "metadata": { "name": "queryServiceFromExplore", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-04-02T10:00:33Z" }, "spec": { @@ -2519,7 +2695,7 @@ { "metadata": { "name": "queryServiceFromUI", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -2532,7 +2708,7 @@ { "metadata": { "name": "queryServiceRewrite", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -2545,7 +2721,7 @@ { "metadata": { "name": "recordedQueriesMulti", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-06-14T12:34:22Z" }, "spec": { @@ -2555,10 +2731,25 @@ "expression": "true" } }, + { + "metadata": { + "name": "recoveryThreshold", + "resourceVersion": "1743693517832", + "creationTimestamp": "2023-10-10T14:51:50Z", + "deletionTimestamp": "2025-05-05T08:21:21Z" + }, + "spec": { + "description": "Enables feature recovery threshold (aka hysteresis) for threshold server-side expression", + "stage": "GA", + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true, + "expression": "true" + } + }, { "metadata": { "name": "refactorVariablesTimeRange", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-06-06T13:12:09Z" }, "spec": { @@ -2571,7 +2762,7 @@ { "metadata": { "name": "regressionTransformation", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-11-24T14:49:16Z" }, "spec": { @@ -2584,7 +2775,7 @@ { "metadata": { "name": "reloadDashboardsOnParamsChange", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-10-25T12:56:54Z" }, "spec": { @@ -2598,7 +2789,7 @@ { "metadata": { "name": "renderAuthJWT", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-04-03T16:53:38Z" }, "spec": { @@ -2611,7 +2802,7 @@ { "metadata": { "name": "rendererDisableAppPluginsPreload", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-02-24T14:43:06Z" }, "spec": { @@ -2626,7 +2817,7 @@ { "metadata": { "name": "reportingRetries", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-08-31T07:47:47Z" }, "spec": { @@ -2639,7 +2830,7 @@ { "metadata": { "name": "reportingUseRawTimeRange", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-11-14T20:08:03Z" }, "spec": { @@ -2652,7 +2843,7 @@ { "metadata": { "name": "rolePickerDrawer", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-09-26T12:51:38Z" }, "spec": { @@ -2664,7 +2855,7 @@ { "metadata": { "name": "scopeApi", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-11-27T07:58:25Z" }, "spec": { @@ -2678,7 +2869,7 @@ { "metadata": { "name": "scopeFilters", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-03-05T15:41:19Z" }, "spec": { @@ -2706,7 +2897,7 @@ { "metadata": { "name": "secretsManagementAppPlatform", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-03-19T09:25:14Z" }, "spec": { @@ -2718,7 +2909,7 @@ { "metadata": { "name": "showDashboardValidationWarnings", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2022-10-14T13:51:05Z" }, "spec": { @@ -2730,7 +2921,7 @@ { "metadata": { "name": "sqlDatasourceDatabaseSelection", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-06-06T16:28:52Z" }, "spec": { @@ -2744,7 +2935,7 @@ { "metadata": { "name": "sqlExpressions", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-02-27T21:16:00Z" }, "spec": { @@ -2756,7 +2947,7 @@ { "metadata": { "name": "sseGroupByDatasource", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-09-07T20:02:07Z" }, "spec": { @@ -2768,7 +2959,7 @@ { "metadata": { "name": "ssoSettingsApi", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-11-08T09:50:01Z" }, "spec": { @@ -2782,7 +2973,7 @@ { "metadata": { "name": "ssoSettingsLDAP", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-06-18T11:31:27Z" }, "spec": { @@ -2796,7 +2987,7 @@ { "metadata": { "name": "ssoSettingsSAML", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-03-14T11:04:45Z" }, "spec": { @@ -2810,7 +3001,7 @@ { "metadata": { "name": "storage", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2022-03-17T17:19:23Z" }, "spec": { @@ -2822,7 +3013,7 @@ { "metadata": { "name": "tableNextGen", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-03-26T03:57:57Z" }, "spec": { @@ -2834,7 +3025,7 @@ { "metadata": { "name": "tableSharedCrosshair", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-12-13T09:33:14Z" }, "spec": { @@ -2847,7 +3038,7 @@ { "metadata": { "name": "teamHttpHeadersMimir", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-01-13T10:42:47Z" }, "spec": { @@ -2860,7 +3051,7 @@ { "metadata": { "name": "templateVariablesUsesCombobox", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-01-31T09:53:13Z" }, "spec": { @@ -2873,7 +3064,7 @@ { "metadata": { "name": "timeRangeProvider", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-10-22T10:52:33Z" }, "spec": { @@ -2885,7 +3076,7 @@ { "metadata": { "name": "tlsMemcached", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-05-09T19:12:08Z" }, "spec": { @@ -2895,10 +3086,25 @@ "expression": "true" } }, + { + "metadata": { + "name": "traceQLStreaming", + "resourceVersion": "1743693517832", + "creationTimestamp": "2023-07-26T13:33:16Z", + "deletionTimestamp": "2025-04-09T12:57:07Z" + }, + "spec": { + "description": "Enables response streaming of TraceQL queries of the Tempo data source", + "stage": "GA", + "codeowner": "@grafana/observability-traces-and-profiling", + "frontend": true, + "expression": "false" + } + }, { "metadata": { "name": "transformationsRedesign", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-07-12T16:35:49Z" }, "spec": { @@ -2913,7 +3119,7 @@ { "metadata": { "name": "unifiedHistory", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-12-13T10:41:18Z" }, "spec": { @@ -2940,7 +3146,7 @@ { "metadata": { "name": "unifiedRequestLog", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-03-31T13:38:09Z" }, "spec": { @@ -2954,7 +3160,7 @@ { "metadata": { "name": "unifiedStorageBigObjectsSupport", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-10-17T10:18:29Z" }, "spec": { @@ -2966,7 +3172,7 @@ { "metadata": { "name": "unifiedStorageGrpcConnectionPool", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-03-21T13:24:54Z" }, "spec": { @@ -2980,7 +3186,7 @@ { "metadata": { "name": "unifiedStorageHistoryPruner", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-03-17T10:36:38Z" }, "spec": { @@ -2995,7 +3201,7 @@ { "metadata": { "name": "unifiedStorageSearch", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-09-30T19:46:14Z" }, "spec": { @@ -3009,7 +3215,7 @@ { "metadata": { "name": "unifiedStorageSearchPermissionFiltering", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-01-22T11:38:37Z" }, "spec": { @@ -3024,7 +3230,7 @@ { "metadata": { "name": "unifiedStorageSearchSprinkles", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-12-18T17:00:54Z" }, "spec": { @@ -3038,7 +3244,7 @@ { "metadata": { "name": "unifiedStorageSearchUI", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-12-19T18:21:48Z" }, "spec": { @@ -3052,7 +3258,7 @@ { "metadata": { "name": "useScopesNavigationEndpoint", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-03-31T15:20:00Z" }, "spec": { @@ -3067,7 +3273,7 @@ { "metadata": { "name": "useSessionStorageForRedirection", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-09-23T09:31:23Z" }, "spec": { @@ -3080,7 +3286,7 @@ { "metadata": { "name": "wargamesTesting", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2023-09-13T18:32:01Z" }, "spec": { @@ -3092,7 +3298,7 @@ { "metadata": { "name": "xrayApplicationSignals", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2025-04-01T14:42:02Z" }, "spec": { @@ -3107,7 +3313,7 @@ { "metadata": { "name": "zanzana", - "resourceVersion": "1745491786560", + "resourceVersion": "1743693517832", "creationTimestamp": "2024-06-19T13:59:47Z" }, "spec": { diff --git a/public/app/features/datasources/components/EditDataSource.tsx b/public/app/features/datasources/components/EditDataSource.tsx index 7fe90eba063..e8c56430ec1 100644 --- a/public/app/features/datasources/components/EditDataSource.tsx +++ b/public/app/features/datasources/components/EditDataSource.tsx @@ -1,5 +1,4 @@ import { AnyAction } from '@reduxjs/toolkit'; -import { cloneDeep } from 'lodash'; import { useMemo } from 'react'; import * as React from 'react'; @@ -199,8 +198,8 @@ export function EditDataSourceView({
onOptionsChange({ diff --git a/public/app/features/plugins/extensions/logs/log.ts b/public/app/features/plugins/extensions/logs/log.ts index 252171c2ffd..8dc312dda16 100644 --- a/public/app/features/plugins/extensions/logs/log.ts +++ b/public/app/features/plugins/extensions/logs/log.ts @@ -3,7 +3,7 @@ import { nanoid } from 'nanoid'; import { Observable, ReplaySubject } from 'rxjs'; import { Labels, LogLevel } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { config, createMonitoringLogger } from '@grafana/runtime'; export type ExtensionsLogItem = { level: LogLevel; @@ -18,6 +18,7 @@ export type ExtensionsLogItem = { const channelName = 'ui-extension-logs'; const logsNumberLimit = 1000; const logsRetentionTime = 1000 * 60 * 10; +const monitoringLogger = createMonitoringLogger(channelName); export class ExtensionsLog { private baseLabels: Labels | undefined; @@ -39,11 +40,15 @@ export class ExtensionsLog { } warning(message: string, labels?: Labels): void { + monitoringLogger.logWarning(message, { ...this.baseLabels, ...labels }); config.buildInfo.env === 'development' && console.warn(message, { ...this.baseLabels, ...labels }); this.log(LogLevel.warning, message, labels); } error(message: string, labels?: Labels): void { + // TODO: If Faro has console instrumentation, then the following will track the same error message twice + // (first: `monitoringLogger.logError()`, second: `console.error()` which gets picked up by Faro) + monitoringLogger.logError(new Error(message), { ...this.baseLabels, ...labels }); console.error(message, { ...this.baseLabels, ...labels }); this.log(LogLevel.error, message, labels); } diff --git a/public/app/features/plugins/extensions/usePluginComponent.test.tsx b/public/app/features/plugins/extensions/usePluginComponent.test.tsx index b2a3d3d3435..51583987271 100644 --- a/public/app/features/plugins/extensions/usePluginComponent.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponent.test.tsx @@ -13,27 +13,27 @@ import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry' import { PluginExtensionRegistries } from './registry/types'; import { useLoadAppPlugins } from './useLoadAppPlugins'; import { usePluginComponent } from './usePluginComponent'; -import { isGrafanaDevMode, wrapWithPluginContext } from './utils'; +import { isGrafanaDevMode } from './utils'; jest.mock('./useLoadAppPlugins'); -jest.mock('app/features/plugins/pluginSettings', () => ({ - getPluginSettings: jest.fn().mockResolvedValue({ - id: 'my-app-plugin', - enabled: true, - jsonData: {}, - type: 'panel', - name: 'My App Plugin', - module: 'app/plugins/my-app-plugin/module', - }), -})); - jest.mock('./utils', () => ({ ...jest.requireActual('./utils'), // Manually set the dev mode to false // (to make sure that by default we are testing a production scneario) isGrafanaDevMode: jest.fn().mockReturnValue(false), - wrapWithPluginContext: jest.fn().mockImplementation((_, component: React.ReactNode) => component), +})); + +// See: public/app/features/plugins/extensions/utils.tsx for implementation details +jest.mock('react-use', () => ({ + ...jest.requireActual('react-use'), + useAsync: jest.fn().mockImplementation(() => ({ + error: null, + loading: false, + value: { + id: 'my-app-plugin', + }, + })), })); jest.mock('./logs/log', () => { @@ -98,8 +98,6 @@ describe('usePluginComponent()', () => { jest.mocked(isGrafanaDevMode).mockReturnValue(false); resetLogMock(log); - jest.mocked(wrapWithPluginContext).mockClear(); - pluginMeta = { id: pluginId, name: 'Extensions App', @@ -212,9 +210,11 @@ describe('usePluginComponent()', () => { }); }); - expect(wrapWithPluginContext).toHaveBeenCalledTimes(0); + jest.mocked(isGrafanaDevMode).mockClear(); + expect(isGrafanaDevMode).toHaveBeenCalledTimes(0); renderHook(() => usePluginComponent(exposedComponentId), { wrapper }); - await waitFor(() => expect(wrapWithPluginContext).toHaveBeenCalledTimes(1)); + // The registryState is undefined in the first render, so the isGrafanaDevMode() is called twice + await waitFor(() => expect(isGrafanaDevMode).toHaveBeenCalledTimes(2)); }); it('should not validate the meta-info in production mode', () => { @@ -332,4 +332,109 @@ describe('usePluginComponent()', () => { expect(result.current.component).not.toBe(null); expect(log.warning).not.toHaveBeenCalled(); }); + + it('should pass a read-only copy of the props (in dev mode)', async () => { + config.buildInfo.env = 'development'; + + type Props = { + a: { + b: { + c: string; + }; + }; + override?: boolean; + }; + + registries.exposedComponentsRegistry.register({ + pluginId, + configs: [ + { + ...exposedComponentConfig, + // @ts-expect-error - The registry shouldn't be used this way + component: (props: Props) => { + if (props.override) { + props.a.b.c = 'baz'; + } + + return Foo; + }, + }, + ], + }); + + const originalProps = { + a: { + b: { + c: 'bar', + }, + }, + }; + + const { result } = renderHook(() => usePluginComponent(exposedComponentId), { wrapper }); + const Component = result.current.component; + + // Should render normally if it doesn't mutate the props + const rendered = render(Component && ); + expect(rendered.getByText('Foo')).toBeVisible(); + + // Should throw an error if it mutates the props + jest.spyOn(console, 'error').mockImplementation(() => {}); + expect(() => render(Component && )).toThrow( + TypeError("'set' on proxy: trap returned falsish for property 'c'") + ); + jest.spyOn(console, 'error').mockRestore(); + }); + + it('should pass a writable copy of the props (in production mode)', async () => { + config.buildInfo.env = 'production'; + + type Props = { + a: { + b: { + c: string; + }; + }; + override?: boolean; + }; + + registries.exposedComponentsRegistry.register({ + pluginId, + configs: [ + { + ...exposedComponentConfig, + // @ts-expect-error - The registry shouldn't be used this way + component: (props: Props) => { + if (props.override) { + props.a.b.c = 'baz'; + } + + return Foo; + }, + }, + ], + }); + + const originalProps = { + a: { + b: { + c: 'bar', + }, + }, + }; + + const { result } = renderHook(() => usePluginComponent(exposedComponentId), { wrapper }); + const Component = result.current.component; + + // Should render normally if it doesn't mutate the props + const rendered = render(Component && ); + expect(rendered.getByText('Foo')).toBeVisible(); + + // Should throw an error if it mutates the props + expect(() => render(Component && )).not.toThrow(); + + // Should log a warning + expect(log.warning).toHaveBeenCalledWith('Attempted to mutate object property "c"', { + stack: expect.any(String), + }); + }); }); diff --git a/public/app/features/plugins/extensions/usePluginComponents.test.tsx b/public/app/features/plugins/extensions/usePluginComponents.test.tsx index 03aad063d53..7f306ff3335 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.test.tsx @@ -1,6 +1,8 @@ import { act, render, renderHook, screen } from '@testing-library/react'; +import React from 'react'; import { PluginContextProvider, PluginMeta, PluginType } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; import { log } from './logs/log'; @@ -12,19 +14,9 @@ import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry' import { PluginExtensionRegistries } from './registry/types'; import { useLoadAppPlugins } from './useLoadAppPlugins'; import { usePluginComponents } from './usePluginComponents'; -import { isGrafanaDevMode, wrapWithPluginContext } from './utils'; +import { isGrafanaDevMode } from './utils'; jest.mock('./useLoadAppPlugins'); -jest.mock('app/features/plugins/pluginSettings', () => ({ - getPluginSettings: jest.fn().mockResolvedValue({ - id: 'my-app-plugin', - enabled: true, - jsonData: {}, - type: 'panel', - name: 'My App Plugin', - module: 'app/plugins/my-app-plugin/module', - }), -})); jest.mock('./utils', () => ({ ...jest.requireActual('./utils'), @@ -32,7 +24,6 @@ jest.mock('./utils', () => ({ // Manually set the dev mode to false // (to make sure that by default we are testing a production scneario) isGrafanaDevMode: jest.fn().mockReturnValue(false), - wrapWithPluginContext: jest.fn().mockImplementation((_, component: React.ReactNode) => component), })); jest.mock('./logs/log', () => { @@ -45,14 +36,28 @@ jest.mock('./logs/log', () => { }; }); +// See: public/app/features/plugins/extensions/utils.tsx for implementation details +jest.mock('react-use', () => ({ + ...jest.requireActual('react-use'), + useAsync: jest.fn().mockImplementation(() => ({ + error: null, + loading: false, + value: { + id: 'my-app-plugin', + }, + })), +})); + describe('usePluginComponents()', () => { let registries: PluginExtensionRegistries; let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element; let pluginMeta: PluginMeta; const pluginId = 'myorg-extensions-app'; const extensionPointId = `${pluginId}/extension-point/v1`; + const originalBuildInfoEnv = config.buildInfo.env; beforeEach(() => { + config.buildInfo.env = originalBuildInfoEnv; jest.mocked(isGrafanaDevMode).mockReturnValue(false); jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false }); @@ -64,8 +69,6 @@ describe('usePluginComponents()', () => { addedFunctionsRegistry: new AddedFunctionsRegistry(), }; - jest.mocked(wrapWithPluginContext).mockClear(); - pluginMeta = { id: pluginId, name: 'Extensions App', @@ -109,6 +112,11 @@ describe('usePluginComponents()', () => { ); }); + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + it('should return an empty array if there are no extensions registered for the extension point', () => { const { result } = renderHook( () => @@ -203,6 +211,128 @@ describe('usePluginComponents()', () => { }); }); + it('should pass a read only copy of the props to the components (in dev mode)', async () => { + config.buildInfo.env = 'development'; + + type Props = { + foo: { + foo2: { + foo3: { + foo4: string; + }; + }; + }; + override?: boolean; + }; + + const originalFoo = { + foo2: { + foo3: { + foo4: 'bar', + }, + }, + }; + + registries.addedComponentsRegistry.register({ + pluginId, + configs: [ + { + targets: extensionPointId, + title: '1', + description: '1', + // @ts-ignore - The register() method is not designed to be called directly like this, and because of that it doesn't have a way to set the type of the Props + component: ({ foo, override = false }: Props) => { + // Trying to override the prop + if (override) { + const foo3 = foo.foo2.foo3; + foo3.foo4 = 'baz'; + } + + return Foo; + }, + }, + ], + }); + + // Check if it returns the components + const { result } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper }); + expect(result.current.components.length).toBe(1); + + const Component = result.current.components[0]; + + // Should be possible to render the component if it doesn't want to change the props + const rendered = render(); + expect(rendered.getByText('Foo')).toBeVisible(); + + // Check if it throws a TypeError due to trying to change the prop + jest.spyOn(console, 'error').mockImplementation(() => {}); + expect(() => render()).toThrow(TypeError); + + // Check if the original property hasn't been changed + expect(originalFoo.foo2.foo3.foo4).toBe('bar'); + }); + + it('should pass a copy of the props to the components (in production mode)', async () => { + type Props = { + foo: { + foo2: { + foo3: { + foo4: string; + }; + }; + }; + override?: boolean; + }; + + const originalFoo = { + foo2: { + foo3: { + foo4: 'bar', + }, + }, + }; + + registries.addedComponentsRegistry.register({ + pluginId, + configs: [ + { + targets: extensionPointId, + title: '1', + description: '1', + // @ts-ignore - The register() method is not designed to be called directly like this, and because of that it doesn't have a way to set the type of the Props + component: ({ foo, override = false }: Props) => { + // Trying to override the prop + if (override) { + const foo3 = foo.foo2.foo3; + foo3.foo4 = 'baz'; + } + + return Foo; + }, + }, + ], + }); + + // Check if it returns the components + const { result } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper }); + expect(result.current.components.length).toBe(1); + + const Component = result.current.components[0]; + + // Should be possible to render the component if it doesn't want to change the props + const rendered = render(); + expect(rendered.getByText('Foo')).toBeVisible(); + + // Should also render the component if it wants to change the props + expect(() => render()).not.toThrow(); + expect(log.warning).toHaveBeenCalledWith(`Attempted to mutate object property "foo4"`, { + stack: expect.any(String), + }); + + // Check if the original property hasn't been changed + expect(originalFoo.foo2.foo3.foo4).toBe('bar'); + }); + it('should dynamically update the extensions registered for a certain extension point', () => { let { result, rerender } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper }); diff --git a/public/app/features/plugins/extensions/usePluginComponents.tsx b/public/app/features/plugins/extensions/usePluginComponents.tsx index e453b306d84..58d95289c15 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.tsx @@ -38,10 +38,12 @@ export function usePluginComponents({ extensionPointId, }); + // Only log error for an invalid `extensionPointId` in DEV mode if (enableRestrictions && !isExtensionPointIdValid({ extensionPointId, pluginId })) { pointLog.error(errors.INVALID_EXTENSION_POINT_ID); } + // Don't show extensions if the extension-point misses meta info (plugin.json) in DEV mode if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) { pointLog.error(errors.EXTENSION_POINT_META_INFO_MISSING); return { @@ -85,12 +87,12 @@ export function usePluginComponents({ }, [extensionPointId, limitPerPlugin, pluginContext, registryState, isLoadingAppPlugins]); } -// exported so it can be used in tests export function createComponentWithMeta( registryItem: AddedComponentRegistryItem, extensionPointId: string ): ComponentTypeWithExtensionMeta { const { component: Component, ...config } = registryItem; + function ComponentWithMeta(props: Props) { return ; } diff --git a/public/app/features/plugins/extensions/utils.test.tsx b/public/app/features/plugins/extensions/utils.test.tsx index 3075569aaba..b2cf153cb7d 100644 --- a/public/app/features/plugins/extensions/utils.test.tsx +++ b/public/app/features/plugins/extensions/utils.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import { type Unsubscribable } from 'rxjs'; import { dateTime, usePluginContext, PluginLoadingStrategy } from '@grafana/data'; @@ -7,6 +7,7 @@ import appEvents from 'app/core/app_events'; import { ShowModalReactEvent } from 'app/types/events'; import { log } from './logs/log'; +import { resetLogMock } from './logs/testUtils'; import { deepFreeze, handleErrorsInFn, @@ -19,6 +20,10 @@ import { getAppPluginIdFromExposedComponentId, getAppPluginDependencies, getExtensionPointPluginMeta, + getMutationObserverProxy, + readOnlyCopy, + isReadOnlyProxy, + isMutationObserverProxy, } from './utils'; jest.mock('app/features/plugins/pluginSettings', () => ({ @@ -26,6 +31,16 @@ jest.mock('app/features/plugins/pluginSettings', () => ({ getPluginSettings: () => Promise.resolve({ info: { version: '1.0.0' } }), })); +jest.mock('./logs/log', () => { + const { createLogMock } = jest.requireActual('./logs/testUtils'); + const original = jest.requireActual('./logs/log'); + + return { + ...original, + log: createLogMock(), + }; +}); + describe('Plugin Extensions / Utils', () => { describe('deepFreeze()', () => { test('should not fail when called with primitive values', () => { @@ -228,7 +243,7 @@ describe('Plugin Extensions / Utils', () => { expect(() => { proxy.a = 'b'; - }).toThrowError(TypeError); + }).toThrow(TypeError); }); it('should not be possible to modify values in proxied array', () => { @@ -236,7 +251,7 @@ describe('Plugin Extensions / Utils', () => { expect(() => { proxy[0] = 2; - }).toThrowError(TypeError); + }).toThrow(TypeError); }); it('should not be possible to modify nested objects in proxied object', () => { @@ -249,7 +264,58 @@ describe('Plugin Extensions / Utils', () => { expect(() => { proxy.a.c = 'testing'; - }).toThrowError(TypeError); + }).toThrow(TypeError); + }); + + // This is to record what we are not able to do currently. + // (Due to Proxy.get() invariants limitations: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/get#invariants) + it('should not work with any objects that are already frozen', () => { + const obj = { + a: { + b: { + c: { + d: 'd', + }, + }, + }, + }; + + Object.freeze(obj); + Object.freeze(obj.a); + Object.freeze(obj.a.b); + + const proxy = getReadOnlyProxy(obj); + + expect(() => { + proxy.a.b.c.d = 'testing'; + }).toThrow( + "'get' on proxy: property 'a' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '#' but got '#')" + ); + + expect(obj.a.b.c.d).toBe('d'); + }); + + it('should throw a TypeError if a proxied object is trying to be frozen', () => { + const obj = { + a: { + b: { + c: { + d: 'd', + }, + }, + }, + }; + + const proxy = getReadOnlyProxy(obj); + + expect(() => Object.freeze(proxy)).toThrow(TypeError); + expect(() => Object.freeze(proxy.a)).toThrow(TypeError); + expect(() => Object.freeze(proxy.a.b)).toThrow(TypeError); + + // Check if the original object is not frozen + expect(Object.isFrozen(obj)).toBe(false); + expect(Object.isFrozen(obj.a)).toBe(false); + expect(Object.isFrozen(obj.a.b)).toBe(false); }); it('should not be possible to modify nested arrays in proxied object', () => { @@ -262,7 +328,7 @@ describe('Plugin Extensions / Utils', () => { expect(() => { proxy.a.c[0] = 'testing'; - }).toThrowError(TypeError); + }).toThrow(TypeError); }); it('should be possible to modify source object', () => { @@ -320,6 +386,178 @@ describe('Plugin Extensions / Utils', () => { }); }); + describe('getMutationObserverProxy()', () => { + it('should not be possible to modify values in proxied object, but logs a warning', () => { + const proxy = getMutationObserverProxy({ a: 'a' }); + + expect(() => { + proxy.a = 'b'; + }).not.toThrow(); + + expect(log.warning).toHaveBeenCalledWith(`Attempted to mutate object property "a"`, { + stack: expect.any(String), + }); + + expect(proxy.a).toBe('b'); + }); + + it('should be possible to set new values, but logs a warning', () => { + const obj: { a: string; b?: string } = { a: 'a' }; + const proxy = getMutationObserverProxy(obj); + + expect(() => { + Object.defineProperty(proxy, 'b', { + value: 'b', + writable: false, + }); + }).not.toThrow(); + + expect(log.warning).toHaveBeenCalledWith(`Attempted to define object property "b"`, { + stack: expect.any(String), + }); + + expect(proxy.b).toBe('b'); + }); + + it('should be possible to delete properties, but logs a warning', () => { + const proxy = getMutationObserverProxy({ + a: { + c: 'c', + }, + b: 'b', + }); + + expect(() => { + // @ts-ignore - This is to test the logic + delete proxy.a.c; + }).not.toThrow(); + + expect(log.warning).toHaveBeenCalledWith(`Attempted to delete object property "c"`, { + stack: expect.any(String), + }); + + expect(proxy.a.c).toBeUndefined(); + }); + }); + + describe('readOnlyCopy()', () => { + const originalEnv = config.buildInfo.env; + + beforeEach(() => { + jest.spyOn(console, 'warn').mockImplementation(); + config.featureToggles.extensionsReadOnlyProxy = false; + }); + + afterEach(() => { + config.buildInfo.env = originalEnv; + jest.mocked(console.warn).mockClear(); + }); + + it('should return the same value for primitive types', () => { + expect(readOnlyCopy(1)).toBe(1); + expect(readOnlyCopy('a')).toBe('a'); + expect(readOnlyCopy(true)).toBe(true); + expect(readOnlyCopy(false)).toBe(false); + expect(readOnlyCopy(null)).toBe(null); + expect(readOnlyCopy(undefined)).toBe(undefined); + }); + + it('should return a read-only proxy of the original object if the feature flag is enabled', () => { + config.featureToggles.extensionsReadOnlyProxy = true; + + const obj = { a: 'a' }; + const copy = readOnlyCopy(obj); + + expect(copy).not.toBe(obj); + expect(copy.a).toBe('a'); + expect(isReadOnlyProxy(copy)).toBe(true); + expect(() => { + copy.a = 'b'; + }).toThrow(TypeError); + }); + + it('should return a read-only proxy of a deep-copy of the original object in dev mode', () => { + config.featureToggles.extensionsReadOnlyProxy = false; + config.buildInfo.env = 'development'; + + const obj = { a: 'a' }; + const copy = readOnlyCopy(obj); + + expect(copy).not.toBe(obj); + expect(copy.a).toBe('a'); + expect(isReadOnlyProxy(copy)).toBe(true); + expect(() => { + copy.a = 'b'; + }).toThrow(TypeError); + + // Also test that we can handle frozen objects + // (This is not possible with getReadOnlyProxy, as it throws an error when the object is already frozen) + const obj2 = { + a: { + b: { + c: { + d: 'd', + }, + }, + }, + }; + + Object.freeze(obj2); + Object.freeze(obj2.a); + Object.freeze(obj2.a.b); + + const copy2 = readOnlyCopy(obj2); + + expect(() => { + copy2.a.b.c.d = 'testing'; + }).toThrow("'set' on proxy: trap returned falsish for property 'd'"); + + expect(copy2.a.b.c.d).toBe('d'); + }); + + it('should return a writable deep-copy of the original object in production mode', () => { + config.featureToggles.extensionsReadOnlyProxy = false; + config.buildInfo.env = 'production'; + + const obj = { a: 'a' }; + const copy = readOnlyCopy(obj); + + expect(copy).not.toBe(obj); + expect(copy.a).toBe('a'); + expect(isMutationObserverProxy(copy)).toBe(true); + expect(() => { + copy.a = 'b'; + }).not.toThrow(); + + expect(log.warning).toHaveBeenCalledWith(`Attempted to mutate object property "a"`, { + stack: expect.any(String), + }); + + expect(copy.a).toBe('b'); + }); + + it('should allow freezing the object in production mode', () => { + config.featureToggles.extensionsReadOnlyProxy = false; + config.buildInfo.env = 'production'; + + const obj = { a: 'a', b: { c: 'c' } }; + const copy = readOnlyCopy(obj); + + expect(() => { + Object.freeze(copy); + Object.freeze(copy.b); + }).not.toThrow(); + + expect(Object.isFrozen(copy)).toBe(true); + expect(Object.isFrozen(copy.b)).toBe(true); + expect(copy.b).toEqual({ c: 'c' }); + + expect(log.warning).toHaveBeenCalledWith(`Attempted to define object property "a"`, { + stack: expect.any(String), + }); + }); + }); + describe('createOpenModalFunction()', () => { let renderModalSubscription: Unsubscribable | undefined; @@ -417,15 +655,24 @@ describe('Plugin Extensions / Utils', () => { }); }); - describe('wrapExtensionComponentWithContext()', () => { + describe('wrapWithPluginContext()', () => { type ExampleComponentProps = { - audience?: string; + a: { + b: { + c: string; + }; + }; + override?: boolean; }; const ExampleComponent = (props: ExampleComponentProps) => { const pluginContext = usePluginContext(); - const audience = props.audience || 'Grafana'; + const audience = props.a.b.c || 'Grafana'; + + if (props.override) { + props.a.b.c = 'OVERRIDE'; + } return (
@@ -434,11 +681,15 @@ describe('Plugin Extensions / Utils', () => { ); }; + beforeEach(() => { + resetLogMock(log); + }); + it('should make the plugin context available for the wrapped component', async () => { const pluginId = 'grafana-worldmap-panel'; const Component = wrapWithPluginContext(pluginId, ExampleComponent, log); - render(); + render(); expect(await screen.findByText('Hello Grafana!')).toBeVisible(); expect(screen.getByText('Version: 1.0.0')).toBeVisible(); @@ -448,11 +699,52 @@ describe('Plugin Extensions / Utils', () => { const pluginId = 'grafana-worldmap-panel'; const Component = wrapWithPluginContext(pluginId, ExampleComponent, log); - render(); + render(); - expect(await screen.findByText('Hello folks!')).toBeVisible(); + expect(await screen.findByText('Hello Grafana!')).toBeVisible(); expect(screen.getByText('Version: 1.0.0')).toBeVisible(); }); + + it('should not be possible to mutate the props in development mode, and it also throws an error', async () => { + config.buildInfo.env = 'development'; + const pluginId = 'grafana-worldmap-panel'; + const Component = wrapWithPluginContext(pluginId, ExampleComponent, log); + const props = { a: { b: { c: 'Grafana' } } }; + + jest.spyOn(console, 'error').mockImplementation(); + + await expect(async () => { + await act(async () => { + render(); + }); + }).rejects.toThrow(`'set' on proxy: trap returned falsish for property 'c'`); + + // Logs an error + expect(console.error).toHaveBeenCalledWith(expect.any(String)); + + // Not able to mutate the props in development mode + expect(props.a.b.c).toBe('Grafana'); + }); + + it('should not be possible to mutate the props in production mode either, but it logs a warning', async () => { + config.buildInfo.env = 'production'; + const pluginId = 'grafana-worldmap-panel'; + const Component = wrapWithPluginContext(pluginId, ExampleComponent, log); + const props = { a: { b: { c: 'Grafana' } } }; + + render(); + + expect(await screen.findByText('Hello Grafana!')).toBeVisible(); + + // Logs a warning + expect(log.warning).toHaveBeenCalledTimes(1); + expect(log.warning).toHaveBeenCalledWith(`Attempted to mutate object property "c"`, { + stack: expect.any(String), + }); + + // Not able to mutate the props in production mode either + expect(props.a.b.c).toBe('Grafana'); + }); }); describe('getAppPluginConfigs()', () => { diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx index dbf88a26156..249b4d295e4 100644 --- a/public/app/features/plugins/extensions/utils.tsx +++ b/public/app/features/plugins/extensions/utils.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { isArray, isObject } from 'lodash'; +import { cloneDeep, isArray, isObject } from 'lodash'; import * as React from 'react'; import { useAsync } from 'react-use'; @@ -85,7 +85,7 @@ export const wrapWithPluginContext = (pluginId: string, Component: React.Com return ( - + ); }; @@ -163,7 +163,15 @@ export function generateExtensionId(pluginId: string, extensionPointId: string, .toString(); } -const _isProxy = Symbol('isReadOnlyProxy'); +const _isReadOnlyProxy = Symbol('isReadOnlyProxy'); +const _isMutationObserverProxy = Symbol('isMutationObserverProxy'); + +export class ReadOnlyProxyError extends Error { + constructor(message?: string) { + super(message ?? 'Mutating a read-only proxy object'); + this.name = 'ReadOnlyProxyError'; + } +} /** * Returns a proxy that wraps the given object in a way that makes it read only. @@ -185,7 +193,7 @@ export function getReadOnlyProxy(obj: T): T { isExtensible: () => false, set: () => false, get(target, prop, receiver) { - if (prop === _isProxy) { + if (prop === _isReadOnlyProxy) { return true; } @@ -210,12 +218,102 @@ export function getReadOnlyProxy(obj: T): T { }); } +/** + * Returns a proxy that logs any attempted mutation to the original object. + * + * @param obj The object to observe + * @returns A new proxy object that logs any attempted mutation to the original object + */ +export function getMutationObserverProxy(obj: T, _log: ExtensionsLog = log): T { + if (!obj || typeof obj !== 'object' || isMutationObserverProxy(obj)) { + return obj; + } + + const cache = new WeakMap(); + + return new Proxy(obj, { + deleteProperty(target, prop) { + _log.warning(`Attempted to delete object property "${String(prop)}"`, { + stack: new Error().stack ?? '', + }); + Reflect.deleteProperty(target, prop); + return true; + }, + defineProperty(target, prop, descriptor) { + _log.warning(`Attempted to define object property "${String(prop)}"`, { + stack: new Error().stack ?? '', + }); + Reflect.defineProperty(target, prop, descriptor); + return true; + }, + set(target, prop, newValue) { + _log.warning(`Attempted to mutate object property "${String(prop)}"`, { + stack: new Error().stack ?? '', + }); + Reflect.set(target, prop, newValue); + return true; + }, + get(target, prop, receiver) { + if (prop === _isMutationObserverProxy) { + return true; + } + + const value = Reflect.get(target, prop, receiver); + + // Return read-only properties as-is to avoid proxy invariant violations + const descriptor = Reflect.getOwnPropertyDescriptor(target, prop); + if (descriptor && !descriptor.configurable && !descriptor.writable) { + return value; + } + + // This will create a clone of the date time object + // instead of creating a proxy because the underlying + // momentjs object needs to be able to mutate itself. + if (isDateTime(value)) { + return dateTime(value); + } + + if (isObject(value) || isArray(value)) { + if (!cache.has(value)) { + cache.set(value, getMutationObserverProxy(value, _log)); + } + return cache.get(value); + } + + return value; + }, + }); +} + +export function readOnlyCopy(value: T, _log: ExtensionsLog = log): T { + // Primitive types are read-only by default + if (!value || typeof value !== 'object') { + return value; + } + + if (config.featureToggles.extensionsReadOnlyProxy) { + return getReadOnlyProxy(value); + } + + // In dev mode: we return a read-only proxy (throws errors for any mutation), but with a deep-cloned version of the original object (so no interference with other call-sites) + if (isGrafanaDevMode()) { + return getReadOnlyProxy(cloneDeep(value)); + } + + // Default: we return a proxy of a deep-cloned version of the original object, which logs warnings when mutation is attempted + return getMutationObserverProxy(cloneDeep(value), _log); +} + function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null; } export function isReadOnlyProxy(value: unknown): boolean { - return isRecord(value) && value[_isProxy] === true; + return isRecord(value) && value[_isReadOnlyProxy] === true; +} + +export function isMutationObserverProxy(value: unknown): boolean { + return isRecord(value) && value[_isMutationObserverProxy] === true; } export function createAddedLinkConfig(