diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 855ec33a73a..88450ca4420 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -674,8 +674,6 @@ export interface DataSourceInstanceSettings { id: string; name: string; @@ -82,7 +87,7 @@ export interface PluginMeta { signatureType?: PluginSignatureType; signatureOrg?: string; live?: boolean; - angularDetected?: boolean; + angular?: AngularMeta; } interface PluginDependencyInfo { diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 3c6d3a290de..65850046e70 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -16,6 +16,7 @@ import { systemDateFormats, SystemDateFormatSettings, getThemeById, + AngularMeta, } from '@grafana/data'; export interface AzureSettings { @@ -30,7 +31,7 @@ export type AppPluginConfig = { path: string; version: string; preload: boolean; - angularDetected?: boolean; + angular: AngularMeta; }; export class GrafanaBootConfig implements GrafanaConfig { diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index a06d9ca7af2..2d51a715b62 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -71,18 +71,18 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro } panels[panel.ID] = plugins.PanelDTO{ - ID: panel.ID, - Name: panel.Name, - AliasIDs: panel.AliasIDs, - Info: panel.Info, - Module: panel.Module, - BaseURL: panel.BaseURL, - SkipDataQuery: panel.SkipDataQuery, - HideFromList: panel.HideFromList, - ReleaseState: string(panel.State), - Signature: string(panel.Signature), - Sort: getPanelSort(panel.ID), - AngularDetected: panel.AngularDetected, + ID: panel.ID, + Name: panel.Name, + AliasIDs: panel.AliasIDs, + Info: panel.Info, + Module: panel.Module, + BaseURL: panel.BaseURL, + SkipDataQuery: panel.SkipDataQuery, + HideFromList: panel.HideFromList, + ReleaseState: string(panel.State), + Signature: string(panel.Signature), + Sort: getPanelSort(panel.ID), + Angular: panel.Angular, } } @@ -336,8 +336,8 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug Signature: plugin.Signature, Module: plugin.Module, BaseURL: plugin.BaseURL, + Angular: plugin.Angular, } - dsDTO.AngularDetected = plugin.AngularDetected if ds.JsonData == nil { dsDTO.JSONData = make(map[string]any) @@ -419,8 +419,8 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug Signature: ds.Signature, Module: ds.Module, BaseURL: ds.BaseURL, + Angular: ds.Angular, }, - AngularDetected: ds.AngularDetected, } if ds.Name == grafanads.DatasourceName { dto.ID = grafanads.DatasourceID @@ -435,11 +435,11 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug func newAppDTO(plugin pluginstore.Plugin, settings pluginsettings.InfoDTO) *plugins.AppDTO { app := &plugins.AppDTO{ - ID: plugin.ID, - Version: plugin.Info.Version, - Path: plugin.Module, - Preload: false, - AngularDetected: plugin.AngularDetected, + ID: plugin.ID, + Version: plugin.Info.Version, + Path: plugin.Module, + Preload: false, + Angular: plugin.Angular, } if settings.Enabled { diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index 5d40f01f790..5d7efdc561b 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -295,7 +295,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) { Type: plugins.TypeApp, Preload: true, }, - AngularDetected: true, + Angular: plugins.AngularMeta{Detected: true}, }, }, } @@ -308,11 +308,11 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) { expected: settings{ Apps: map[string]*plugins.AppDTO{ "test-app": { - ID: "test-app", - Preload: true, - Path: "/test-app/module.js", - Version: "0.5.0", - AngularDetected: true, + ID: "test-app", + Preload: true, + Path: "/test-app/module.js", + Version: "0.5.0", + Angular: plugins.AngularMeta{Detected: true}, }, }, }, diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 30500e1cae3..e61fb4d174e 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -139,7 +139,7 @@ func (hs *HTTPServer) GetPluginList(c *contextmodel.ReqContext) response.Respons SignatureType: pluginDef.SignatureType, SignatureOrg: pluginDef.SignatureOrg, AccessControl: pluginsMetadata[pluginDef.ID], - AngularDetected: pluginDef.AngularDetected, + AngularDetected: pluginDef.Angular.Detected, } update, exists := hs.pluginsUpdateChecker.HasUpdate(c.Req.Context(), pluginDef.ID) @@ -196,7 +196,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response. SignatureType: plugin.SignatureType, SignatureOrg: plugin.SignatureOrg, SecureJsonFields: map[string]bool{}, - AngularDetected: plugin.AngularDetected, + AngularDetected: plugin.Angular.Detected, } if plugin.IsApp() { diff --git a/pkg/plugins/manager/pipeline/validation/steps.go b/pkg/plugins/manager/pipeline/validation/steps.go index 3025ca60526..502f17140a4 100644 --- a/pkg/plugins/manager/pipeline/validation/steps.go +++ b/pkg/plugins/manager/pipeline/validation/steps.go @@ -95,7 +95,7 @@ func (a *AngularDetector) Validate(ctx context.Context, p *plugins.Plugin) error var err error cctx, canc := context.WithTimeout(ctx, time.Second*10) - p.AngularDetected, err = a.angularInspector.Inspect(cctx, p) + p.Angular.Detected, err = a.angularInspector.Inspect(cctx, p) canc() if err != nil { @@ -103,11 +103,11 @@ func (a *AngularDetector) Validate(ctx context.Context, p *plugins.Plugin) error } // Do not initialize plugins if they're using Angular and Angular support is disabled - if p.AngularDetected && !a.cfg.AngularSupportEnabled { + if p.Angular.Detected && !a.cfg.AngularSupportEnabled { a.log.Error("Refusing to initialize plugin because it's using Angular, which has been disabled", "pluginId", p.ID) return errors.New("angular plugins are not supported") } } - + p.Angular.HideDeprecation = a.cfg.PluginSettings[p.ID]["hide_angular_deprecation"] == "true" return nil } diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 8ccf777af72..0305cdf0aec 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -208,22 +208,23 @@ type PluginMetaDTO struct { Module string `json:"module"` BaseURL string `json:"baseUrl"` + + Angular AngularMeta `json:"angular"` } type DataSourceDTO struct { - ID int64 `json:"id,omitempty"` - UID string `json:"uid,omitempty"` - Type string `json:"type"` - Name string `json:"name"` - PluginMeta *PluginMetaDTO `json:"meta"` - URL string `json:"url,omitempty"` - IsDefault bool `json:"isDefault"` - Access string `json:"access,omitempty"` - Preload bool `json:"preload"` - Module string `json:"module,omitempty"` - JSONData map[string]any `json:"jsonData"` - ReadOnly bool `json:"readOnly"` - AngularDetected bool `json:"angularDetected"` + ID int64 `json:"id,omitempty"` + UID string `json:"uid,omitempty"` + Type string `json:"type"` + Name string `json:"name"` + PluginMeta *PluginMetaDTO `json:"meta"` + URL string `json:"url,omitempty"` + IsDefault bool `json:"isDefault"` + Access string `json:"access,omitempty"` + Preload bool `json:"preload"` + Module string `json:"module,omitempty"` + JSONData map[string]any `json:"jsonData"` + ReadOnly bool `json:"readOnly"` BasicAuth string `json:"basicAuth,omitempty"` WithCredentials bool `json:"withCredentials,omitempty"` @@ -243,26 +244,28 @@ type DataSourceDTO struct { } type PanelDTO struct { - ID string `json:"id"` - Name string `json:"name"` - AliasIDs []string `json:"aliasIds,omitempty"` - Info Info `json:"info"` - HideFromList bool `json:"hideFromList"` - Sort int `json:"sort"` - SkipDataQuery bool `json:"skipDataQuery"` - ReleaseState string `json:"state"` - BaseURL string `json:"baseUrl"` - Signature string `json:"signature"` - Module string `json:"module"` - AngularDetected bool `json:"angularDetected"` + ID string `json:"id"` + Name string `json:"name"` + AliasIDs []string `json:"aliasIds,omitempty"` + Info Info `json:"info"` + HideFromList bool `json:"hideFromList"` + Sort int `json:"sort"` + SkipDataQuery bool `json:"skipDataQuery"` + ReleaseState string `json:"state"` + BaseURL string `json:"baseUrl"` + Signature string `json:"signature"` + Module string `json:"module"` + + Angular AngularMeta `json:"angular"` } type AppDTO struct { - ID string `json:"id"` - Path string `json:"path"` - Version string `json:"version"` - Preload bool `json:"preload"` - AngularDetected bool `json:"angularDetected"` + ID string `json:"id"` + Path string `json:"path"` + Version string `json:"version"` + Preload bool `json:"preload"` + + Angular AngularMeta `json:"angular"` } const ( diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index d8c95a8799d..186520310ec 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -55,7 +55,7 @@ type Plugin struct { Module string BaseURL string - AngularDetected bool + Angular AngularMeta ExternalService *auth.ExternalService @@ -67,6 +67,11 @@ type Plugin struct { mu sync.Mutex } +type AngularMeta struct { + Detected bool `json:"detected"` + HideDeprecation bool `json:"hideDeprecation"` +} + // JSONData represents the plugin's plugin.json type JSONData struct { // Common settings diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index 5878f93c924..736ed206f50 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -1221,9 +1221,9 @@ func TestLoader_AngularClass(t *testing.T) { require.NoError(t, err) require.Len(t, p, 1, "should load 1 plugin") if tc.expAngularDetectionRun { - require.True(t, p[0].AngularDetected, "angular detection should run") + require.True(t, p[0].Angular.Detected, "angular detection should run") } else { - require.False(t, p[0].AngularDetected, "angular detection should not run") + require.False(t, p[0].Angular.Detected, "angular detection should not run") } }) } @@ -1279,6 +1279,49 @@ func TestLoader_Load_Angular(t *testing.T) { } } +func TestLoader_HideAngularDeprecation(t *testing.T) { + fakePluginSource := &fakes.FakePluginSource{ + PluginClassFunc: func(ctx context.Context) plugins.Class { + return plugins.ClassExternal + }, + PluginURIsFunc: func(ctx context.Context) []string { + return []string{filepath.Join(testDataDir(t), "valid-v2-signature")} + }, + } + for _, tc := range []struct { + name string + cfg *config.Cfg + expHideAngularDeprecation bool + }{ + {name: `without "hide_angular_deprecation" setting`, cfg: &config.Cfg{ + AngularSupportEnabled: true, + PluginSettings: setting.PluginSettings{}, + }}, + {name: `with "hide_angular_deprecation" = true`, cfg: &config.Cfg{ + AngularSupportEnabled: true, + PluginSettings: setting.PluginSettings{ + "plugin-id": map[string]string{"hide_angular_deprecation": "true"}, + }}, + }, + {name: `with "hide_angular_deprecation" = false`, cfg: &config.Cfg{ + AngularSupportEnabled: true, + PluginSettings: setting.PluginSettings{ + "plugin-id": map[string]string{"hide_angular_deprecation": "false"}, + }}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + l := newLoaderWithOpts(t, tc.cfg, loaderDepOpts{ + angularInspector: angularinspector.AlwaysAngularFakeInspector, + }) + p, err := l.Load(context.Background(), fakePluginSource) + require.NoError(t, err) + require.Len(t, p, 1, "should load 1 plugin") + require.Equal(t, tc.expHideAngularDeprecation, p[0].Angular.HideDeprecation) + }) + } +} + func TestLoader_Load_NestedPlugins(t *testing.T) { parent := &plugins.Plugin{ JSONData: plugins.JSONData{ diff --git a/pkg/services/pluginsintegration/pluginstore/plugins.go b/pkg/services/pluginsintegration/pluginstore/plugins.go index 7ead33b9ab6..45bd6fb1061 100644 --- a/pkg/services/pluginsintegration/pluginstore/plugins.go +++ b/pkg/services/pluginsintegration/pluginstore/plugins.go @@ -30,7 +30,7 @@ type Plugin struct { Module string BaseURL string - AngularDetected bool + Angular plugins.AngularMeta ExternalService *auth.ExternalService } @@ -72,7 +72,8 @@ func ToGrafanaDTO(p *plugins.Plugin) Plugin { SignatureError: p.SignatureError, Module: p.Module, BaseURL: p.BaseURL, - AngularDetected: p.AngularDetected, ExternalService: p.ExternalService, + + Angular: p.Angular, } } diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx index f9964372880..b8e8d152ee2 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx @@ -102,7 +102,7 @@ export const OptionsPaneOptions = (props: OptionPaneRenderProps) => { return (
- {panel.isAngularPlugin() && ( + {panel.isAngularPlugin() && !plugin.meta.angular?.hideDeprecation && ( - panel.isAngularPlugin() || (panel.datasource?.uid ? isAngularDatasourcePlugin(panel.datasource?.uid) : false) - ); + return this.panels.some((panel) => { + // Return false for plugins that are angular but have angular.hideDeprecation = false + const isAngularPanel = panel.isAngularPlugin() && !panel.plugin?.meta.angular?.hideDeprecation; + let isAngularDs = false; + if (panel.datasource?.uid) { + isAngularDs = isAngularDatasourcePluginAndNotHidden(panel.datasource?.uid); + } + return isAngularPanel || isAngularDs; + }); } } diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 6fb41e402fb..0f20b754ebc 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -620,7 +620,9 @@ export class PanelModel implements DataConfigSource, IPanelModel { } isAngularPlugin(): boolean { - return (this.plugin && this.plugin.angularPanelCtrl) !== undefined || (this.plugin?.meta?.angularDetected ?? false); + return ( + (this.plugin && this.plugin.angularPanelCtrl) !== undefined || (this.plugin?.meta?.angular?.detected ?? false) + ); } destroy() { diff --git a/public/app/features/dashboard/utils/getPanelChromeProps.tsx b/public/app/features/dashboard/utils/getPanelChromeProps.tsx index 9f7c4527804..e6ee45038fa 100644 --- a/public/app/features/dashboard/utils/getPanelChromeProps.tsx +++ b/public/app/features/dashboard/utils/getPanelChromeProps.tsx @@ -5,7 +5,7 @@ import { config, getTemplateSrv, locationService, reportInteraction } from '@gra import { PanelPadding } from '@grafana/ui'; import { InspectTab } from 'app/features/inspector/types'; import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers'; -import { isAngularDatasourcePlugin } from 'app/features/plugins/angularDeprecation/utils'; +import { isAngularDatasourcePluginAndNotHidden } from 'app/features/plugins/angularDeprecation/utils'; import { PanelHeaderTitleItems } from '../dashgrid/PanelHeader/PanelHeaderTitleItems'; import { DashboardModel, PanelModel } from '../state'; @@ -85,9 +85,9 @@ export function getPanelChromeProps(props: CommonProps) { const alertState = props.data.alertState?.state; const isAngularDatasource = props.panel.datasource?.uid - ? isAngularDatasourcePlugin(props.panel.datasource?.uid) + ? isAngularDatasourcePluginAndNotHidden(props.panel.datasource?.uid) : false; - const isAngularPanel = props.panel.isAngularPlugin(); + const isAngularPanel = props.panel.isAngularPlugin() && !props.plugin.meta.angular?.hideDeprecation; const showAngularNotice = (config.featureToggles.angularDeprecationUI ?? false) && (isAngularDatasource || isAngularPanel); diff --git a/public/app/features/plugins/angularDeprecation/utils.ts b/public/app/features/plugins/angularDeprecation/utils.ts index e7ff92a47ff..3c2beb9d1a5 100644 --- a/public/app/features/plugins/angularDeprecation/utils.ts +++ b/public/app/features/plugins/angularDeprecation/utils.ts @@ -1,7 +1,16 @@ +import { DataSourceInstanceSettings } from '@grafana/data'; import { config } from '@grafana/runtime'; +import { DataSourceJsonData } from '@grafana/schema'; + +function getDsInstanceSettingsByUid(dsUid: string): DataSourceInstanceSettings | null { + return Object.values(config.datasources).find((ds) => ds.uid === dsUid) ?? null; +} export function isAngularDatasourcePlugin(dsUid: string): boolean { - return Object.entries(config.datasources).some(([_, ds]) => { - return ds.uid === dsUid && ds.angularDetected; - }); + return getDsInstanceSettingsByUid(dsUid)?.meta.angular?.detected ?? false; +} + +export function isAngularDatasourcePluginAndNotHidden(dsUid: string): boolean { + const settings = getDsInstanceSettingsByUid(dsUid); + return (settings?.meta.angular?.detected && !settings?.meta.angular.hideDeprecation) ?? false; } diff --git a/public/app/features/plugins/importPanelPlugin.ts b/public/app/features/plugins/importPanelPlugin.ts index 053090a1fc2..62dbcb8c4be 100644 --- a/public/app/features/plugins/importPanelPlugin.ts +++ b/public/app/features/plugins/importPanelPlugin.ts @@ -59,7 +59,7 @@ function getPanelPlugin(meta: PanelPluginMeta): Promise { return importPluginModule({ path: meta.module, version: meta.info?.version, - isAngular: meta.angularDetected, + isAngular: meta.angular?.detected, pluginId: meta.id, }) .then((pluginExports) => { diff --git a/public/app/features/plugins/pluginPreloader.ts b/public/app/features/plugins/pluginPreloader.ts index 205a359eaab..a714e39118d 100644 --- a/public/app/features/plugins/pluginPreloader.ts +++ b/public/app/features/plugins/pluginPreloader.ts @@ -25,7 +25,7 @@ async function preload(config: AppPluginConfig): Promise { const { plugin } = await pluginLoader.importPluginModule({ path, version, - isAngular: config.angularDetected, + isAngular: config.angular.detected, pluginId, }); const { extensionConfigs = [] } = plugin; diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index 8b6d628eef9..038b54be04f 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -79,7 +79,7 @@ export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise { if (pluginExports.plugin) { @@ -107,7 +107,7 @@ export function importAppPlugin(meta: PluginMeta): Promise { return importPluginModule({ path: meta.module, version: meta.info?.version, - isAngular: meta.angularDetected, + isAngular: meta.angular?.detected, pluginId: meta.id, }).then((pluginExports) => { const plugin: AppPlugin = pluginExports.plugin ? pluginExports.plugin : new AppPlugin(); diff --git a/public/app/features/query/components/QueryGroup.test.tsx b/public/app/features/query/components/QueryGroup.test.tsx index 2d4b671e061..9a1a885ad11 100644 --- a/public/app/features/query/components/QueryGroup.test.tsx +++ b/public/app/features/query/components/QueryGroup.test.tsx @@ -127,16 +127,16 @@ describe('QueryGroup', () => { describe('Angular deprecation', () => { const deprecationText = /legacy platform based on AngularJS/i; - const oldAngularDetected = mockDS.angularDetected; + const oldAngularDetected = mockDS.meta.angular?.detected ?? false; const oldDatasources = config.datasources; afterEach(() => { - mockDS.angularDetected = oldAngularDetected; + mockDS.meta.angular = { detected: oldAngularDetected, hideDeprecation: false }; config.datasources = oldDatasources; }); it('Should render angular deprecation notice for angular plugins', async () => { - mockDS.angularDetected = true; + mockDS.meta.angular = { detected: true, hideDeprecation: false }; config.datasources[mockDS.name] = mockDS; renderScenario({}); await waitFor(async () => { @@ -145,7 +145,7 @@ describe('QueryGroup', () => { }); it('Should not render angular deprecation notice for non-angular plugins', async () => { - mockDS.angularDetected = false; + mockDS.meta.angular = { detected: false, hideDeprecation: false }; config.datasources[mockDS.name] = mockDS; renderScenario({}); await waitFor(async () => { diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx index 09d133ea850..3d58d544110 100644 --- a/public/app/features/query/components/QueryGroup.tsx +++ b/public/app/features/query/components/QueryGroup.tsx @@ -27,7 +27,7 @@ import { DashboardQueryEditor, isSharedDashboardQuery } from 'app/plugins/dataso import { GrafanaQuery } from 'app/plugins/datasource/grafana/types'; import { QueryGroupOptions } from 'app/types'; -import { isAngularDatasourcePlugin } from '../../plugins/angularDeprecation/utils'; +import { isAngularDatasourcePluginAndNotHidden } from '../../plugins/angularDeprecation/utils'; import { PanelQueryRunner } from '../state/PanelQueryRunner'; import { updateQueries } from '../state/updateQueries'; @@ -255,7 +255,7 @@ export class QueryGroup extends PureComponent { )}
- {dataSource && isAngularDatasourcePlugin(dataSource.uid) && ( + {dataSource && isAngularDatasourcePluginAndNotHidden(dataSource.uid) && (