Plugins: Allow disabling angular deprecation UI for specific plugins (#77026)

* Plugins:Allow disabling angular deprecation UI for specific plugins

* add backend test

* changed test names

* lint

* Removed angular properties from DataSourceDTO

* Update tests

* Move angularDetected and hideAngularDeprecation in angularMeta property

* Fix angular property name in AppPluginConfig

* Fix reference to angularMeta.detected

* Fix hide_angular_deprecation not working for core plugins

* lint
This commit is contained in:
Giuseppe Guerra
2023-11-10 11:44:54 +01:00
committed by GitHub
parent 934456dc1c
commit da117aea1c
21 changed files with 164 additions and 92 deletions

View File

@@ -674,8 +674,6 @@ export interface DataSourceInstanceSettings<T extends DataSourceJsonData = DataS
/** When the name+uid are based on template variables, maintain access to the real values */ /** When the name+uid are based on template variables, maintain access to the real values */
rawRef?: DataSourceRef; rawRef?: DataSourceRef;
angularDetected?: boolean;
} }
/** /**

View File

@@ -52,6 +52,11 @@ export interface PluginError {
pluginType?: PluginType; pluginType?: PluginType;
} }
export interface AngularMeta {
detected: boolean;
hideDeprecation: boolean;
}
export interface PluginMeta<T extends KeyValue = {}> { export interface PluginMeta<T extends KeyValue = {}> {
id: string; id: string;
name: string; name: string;
@@ -82,7 +87,7 @@ export interface PluginMeta<T extends KeyValue = {}> {
signatureType?: PluginSignatureType; signatureType?: PluginSignatureType;
signatureOrg?: string; signatureOrg?: string;
live?: boolean; live?: boolean;
angularDetected?: boolean; angular?: AngularMeta;
} }
interface PluginDependencyInfo { interface PluginDependencyInfo {

View File

@@ -16,6 +16,7 @@ import {
systemDateFormats, systemDateFormats,
SystemDateFormatSettings, SystemDateFormatSettings,
getThemeById, getThemeById,
AngularMeta,
} from '@grafana/data'; } from '@grafana/data';
export interface AzureSettings { export interface AzureSettings {
@@ -30,7 +31,7 @@ export type AppPluginConfig = {
path: string; path: string;
version: string; version: string;
preload: boolean; preload: boolean;
angularDetected?: boolean; angular: AngularMeta;
}; };
export class GrafanaBootConfig implements GrafanaConfig { export class GrafanaBootConfig implements GrafanaConfig {

View File

@@ -82,7 +82,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
ReleaseState: string(panel.State), ReleaseState: string(panel.State),
Signature: string(panel.Signature), Signature: string(panel.Signature),
Sort: getPanelSort(panel.ID), Sort: getPanelSort(panel.ID),
AngularDetected: panel.AngularDetected, Angular: panel.Angular,
} }
} }
@@ -336,8 +336,8 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug
Signature: plugin.Signature, Signature: plugin.Signature,
Module: plugin.Module, Module: plugin.Module,
BaseURL: plugin.BaseURL, BaseURL: plugin.BaseURL,
Angular: plugin.Angular,
} }
dsDTO.AngularDetected = plugin.AngularDetected
if ds.JsonData == nil { if ds.JsonData == nil {
dsDTO.JSONData = make(map[string]any) dsDTO.JSONData = make(map[string]any)
@@ -419,8 +419,8 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug
Signature: ds.Signature, Signature: ds.Signature,
Module: ds.Module, Module: ds.Module,
BaseURL: ds.BaseURL, BaseURL: ds.BaseURL,
Angular: ds.Angular,
}, },
AngularDetected: ds.AngularDetected,
} }
if ds.Name == grafanads.DatasourceName { if ds.Name == grafanads.DatasourceName {
dto.ID = grafanads.DatasourceID dto.ID = grafanads.DatasourceID
@@ -439,7 +439,7 @@ func newAppDTO(plugin pluginstore.Plugin, settings pluginsettings.InfoDTO) *plug
Version: plugin.Info.Version, Version: plugin.Info.Version,
Path: plugin.Module, Path: plugin.Module,
Preload: false, Preload: false,
AngularDetected: plugin.AngularDetected, Angular: plugin.Angular,
} }
if settings.Enabled { if settings.Enabled {

View File

@@ -295,7 +295,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
Type: plugins.TypeApp, Type: plugins.TypeApp,
Preload: true, Preload: true,
}, },
AngularDetected: true, Angular: plugins.AngularMeta{Detected: true},
}, },
}, },
} }
@@ -312,7 +312,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
Preload: true, Preload: true,
Path: "/test-app/module.js", Path: "/test-app/module.js",
Version: "0.5.0", Version: "0.5.0",
AngularDetected: true, Angular: plugins.AngularMeta{Detected: true},
}, },
}, },
}, },

View File

@@ -139,7 +139,7 @@ func (hs *HTTPServer) GetPluginList(c *contextmodel.ReqContext) response.Respons
SignatureType: pluginDef.SignatureType, SignatureType: pluginDef.SignatureType,
SignatureOrg: pluginDef.SignatureOrg, SignatureOrg: pluginDef.SignatureOrg,
AccessControl: pluginsMetadata[pluginDef.ID], AccessControl: pluginsMetadata[pluginDef.ID],
AngularDetected: pluginDef.AngularDetected, AngularDetected: pluginDef.Angular.Detected,
} }
update, exists := hs.pluginsUpdateChecker.HasUpdate(c.Req.Context(), pluginDef.ID) 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, SignatureType: plugin.SignatureType,
SignatureOrg: plugin.SignatureOrg, SignatureOrg: plugin.SignatureOrg,
SecureJsonFields: map[string]bool{}, SecureJsonFields: map[string]bool{},
AngularDetected: plugin.AngularDetected, AngularDetected: plugin.Angular.Detected,
} }
if plugin.IsApp() { if plugin.IsApp() {

View File

@@ -95,7 +95,7 @@ func (a *AngularDetector) Validate(ctx context.Context, p *plugins.Plugin) error
var err error var err error
cctx, canc := context.WithTimeout(ctx, time.Second*10) 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() canc()
if err != nil { 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 // 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) 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") return errors.New("angular plugins are not supported")
} }
} }
p.Angular.HideDeprecation = a.cfg.PluginSettings[p.ID]["hide_angular_deprecation"] == "true"
return nil return nil
} }

View File

@@ -208,6 +208,8 @@ type PluginMetaDTO struct {
Module string `json:"module"` Module string `json:"module"`
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
Angular AngularMeta `json:"angular"`
} }
type DataSourceDTO struct { type DataSourceDTO struct {
@@ -223,7 +225,6 @@ type DataSourceDTO struct {
Module string `json:"module,omitempty"` Module string `json:"module,omitempty"`
JSONData map[string]any `json:"jsonData"` JSONData map[string]any `json:"jsonData"`
ReadOnly bool `json:"readOnly"` ReadOnly bool `json:"readOnly"`
AngularDetected bool `json:"angularDetected"`
BasicAuth string `json:"basicAuth,omitempty"` BasicAuth string `json:"basicAuth,omitempty"`
WithCredentials bool `json:"withCredentials,omitempty"` WithCredentials bool `json:"withCredentials,omitempty"`
@@ -254,7 +255,8 @@ type PanelDTO struct {
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
Signature string `json:"signature"` Signature string `json:"signature"`
Module string `json:"module"` Module string `json:"module"`
AngularDetected bool `json:"angularDetected"`
Angular AngularMeta `json:"angular"`
} }
type AppDTO struct { type AppDTO struct {
@@ -262,7 +264,8 @@ type AppDTO struct {
Path string `json:"path"` Path string `json:"path"`
Version string `json:"version"` Version string `json:"version"`
Preload bool `json:"preload"` Preload bool `json:"preload"`
AngularDetected bool `json:"angularDetected"`
Angular AngularMeta `json:"angular"`
} }
const ( const (

View File

@@ -55,7 +55,7 @@ type Plugin struct {
Module string Module string
BaseURL string BaseURL string
AngularDetected bool Angular AngularMeta
ExternalService *auth.ExternalService ExternalService *auth.ExternalService
@@ -67,6 +67,11 @@ type Plugin struct {
mu sync.Mutex mu sync.Mutex
} }
type AngularMeta struct {
Detected bool `json:"detected"`
HideDeprecation bool `json:"hideDeprecation"`
}
// JSONData represents the plugin's plugin.json // JSONData represents the plugin's plugin.json
type JSONData struct { type JSONData struct {
// Common settings // Common settings

View File

@@ -1221,9 +1221,9 @@ func TestLoader_AngularClass(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Len(t, p, 1, "should load 1 plugin") require.Len(t, p, 1, "should load 1 plugin")
if tc.expAngularDetectionRun { 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 { } 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) { func TestLoader_Load_NestedPlugins(t *testing.T) {
parent := &plugins.Plugin{ parent := &plugins.Plugin{
JSONData: plugins.JSONData{ JSONData: plugins.JSONData{

View File

@@ -30,7 +30,7 @@ type Plugin struct {
Module string Module string
BaseURL string BaseURL string
AngularDetected bool Angular plugins.AngularMeta
ExternalService *auth.ExternalService ExternalService *auth.ExternalService
} }
@@ -72,7 +72,8 @@ func ToGrafanaDTO(p *plugins.Plugin) Plugin {
SignatureError: p.SignatureError, SignatureError: p.SignatureError,
Module: p.Module, Module: p.Module,
BaseURL: p.BaseURL, BaseURL: p.BaseURL,
AngularDetected: p.AngularDetected,
ExternalService: p.ExternalService, ExternalService: p.ExternalService,
Angular: p.Angular,
} }
} }

View File

@@ -102,7 +102,7 @@ export const OptionsPaneOptions = (props: OptionPaneRenderProps) => {
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div className={styles.formBox}> <div className={styles.formBox}>
{panel.isAngularPlugin() && ( {panel.isAngularPlugin() && !plugin.meta.angular?.hideDeprecation && (
<AngularDeprecationPluginNotice <AngularDeprecationPluginNotice
className={styles.angularDeprecationWrapper} className={styles.angularDeprecationWrapper}
showPluginDetailsLink={true} showPluginDetailsLink={true}

View File

@@ -23,7 +23,7 @@ import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui';
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL } from 'app/core/constants'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL } from 'app/core/constants';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object'; import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
import { isAngularDatasourcePlugin } from 'app/features/plugins/angularDeprecation/utils'; import { isAngularDatasourcePluginAndNotHidden } from 'app/features/plugins/angularDeprecation/utils';
import { variableAdapters } from 'app/features/variables/adapters'; import { variableAdapters } from 'app/features/variables/adapters';
import { onTimeRangeUpdated } from 'app/features/variables/state/actions'; import { onTimeRangeUpdated } from 'app/features/variables/state/actions';
import { GetVariables, getVariablesByKey } from 'app/features/variables/state/selectors'; import { GetVariables, getVariablesByKey } from 'app/features/variables/state/selectors';
@@ -1293,10 +1293,15 @@ export class DashboardModel implements TimeModel {
} }
hasAngularPlugins(): boolean { hasAngularPlugins(): boolean {
return this.panels.some( return this.panels.some((panel) => {
(panel) => // Return false for plugins that are angular but have angular.hideDeprecation = false
panel.isAngularPlugin() || (panel.datasource?.uid ? isAngularDatasourcePlugin(panel.datasource?.uid) : 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;
});
} }
} }

View File

@@ -620,7 +620,9 @@ export class PanelModel implements DataConfigSource, IPanelModel {
} }
isAngularPlugin(): boolean { 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() { destroy() {

View File

@@ -5,7 +5,7 @@ import { config, getTemplateSrv, locationService, reportInteraction } from '@gra
import { PanelPadding } from '@grafana/ui'; import { PanelPadding } from '@grafana/ui';
import { InspectTab } from 'app/features/inspector/types'; import { InspectTab } from 'app/features/inspector/types';
import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers'; 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 { PanelHeaderTitleItems } from '../dashgrid/PanelHeader/PanelHeaderTitleItems';
import { DashboardModel, PanelModel } from '../state'; import { DashboardModel, PanelModel } from '../state';
@@ -85,9 +85,9 @@ export function getPanelChromeProps(props: CommonProps) {
const alertState = props.data.alertState?.state; const alertState = props.data.alertState?.state;
const isAngularDatasource = props.panel.datasource?.uid const isAngularDatasource = props.panel.datasource?.uid
? isAngularDatasourcePlugin(props.panel.datasource?.uid) ? isAngularDatasourcePluginAndNotHidden(props.panel.datasource?.uid)
: false; : false;
const isAngularPanel = props.panel.isAngularPlugin(); const isAngularPanel = props.panel.isAngularPlugin() && !props.plugin.meta.angular?.hideDeprecation;
const showAngularNotice = const showAngularNotice =
(config.featureToggles.angularDeprecationUI ?? false) && (isAngularDatasource || isAngularPanel); (config.featureToggles.angularDeprecationUI ?? false) && (isAngularDatasource || isAngularPanel);

View File

@@ -1,7 +1,16 @@
import { DataSourceInstanceSettings } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { DataSourceJsonData } from '@grafana/schema';
function getDsInstanceSettingsByUid(dsUid: string): DataSourceInstanceSettings<DataSourceJsonData> | null {
return Object.values(config.datasources).find((ds) => ds.uid === dsUid) ?? null;
}
export function isAngularDatasourcePlugin(dsUid: string): boolean { export function isAngularDatasourcePlugin(dsUid: string): boolean {
return Object.entries(config.datasources).some(([_, ds]) => { return getDsInstanceSettingsByUid(dsUid)?.meta.angular?.detected ?? false;
return ds.uid === dsUid && ds.angularDetected; }
});
export function isAngularDatasourcePluginAndNotHidden(dsUid: string): boolean {
const settings = getDsInstanceSettingsByUid(dsUid);
return (settings?.meta.angular?.detected && !settings?.meta.angular.hideDeprecation) ?? false;
} }

View File

@@ -59,7 +59,7 @@ function getPanelPlugin(meta: PanelPluginMeta): Promise<PanelPlugin> {
return importPluginModule({ return importPluginModule({
path: meta.module, path: meta.module,
version: meta.info?.version, version: meta.info?.version,
isAngular: meta.angularDetected, isAngular: meta.angular?.detected,
pluginId: meta.id, pluginId: meta.id,
}) })
.then((pluginExports) => { .then((pluginExports) => {

View File

@@ -25,7 +25,7 @@ async function preload(config: AppPluginConfig): Promise<PluginPreloadResult> {
const { plugin } = await pluginLoader.importPluginModule({ const { plugin } = await pluginLoader.importPluginModule({
path, path,
version, version,
isAngular: config.angularDetected, isAngular: config.angular.detected,
pluginId, pluginId,
}); });
const { extensionConfigs = [] } = plugin; const { extensionConfigs = [] } = plugin;

View File

@@ -79,7 +79,7 @@ export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise<Gene
return importPluginModule({ return importPluginModule({
path: meta.module, path: meta.module,
version: meta.info?.version, version: meta.info?.version,
isAngular: meta.angularDetected, isAngular: meta.angular?.detected,
pluginId: meta.id, pluginId: meta.id,
}).then((pluginExports) => { }).then((pluginExports) => {
if (pluginExports.plugin) { if (pluginExports.plugin) {
@@ -107,7 +107,7 @@ export function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> {
return importPluginModule({ return importPluginModule({
path: meta.module, path: meta.module,
version: meta.info?.version, version: meta.info?.version,
isAngular: meta.angularDetected, isAngular: meta.angular?.detected,
pluginId: meta.id, pluginId: meta.id,
}).then((pluginExports) => { }).then((pluginExports) => {
const plugin: AppPlugin = pluginExports.plugin ? pluginExports.plugin : new AppPlugin(); const plugin: AppPlugin = pluginExports.plugin ? pluginExports.plugin : new AppPlugin();

View File

@@ -127,16 +127,16 @@ describe('QueryGroup', () => {
describe('Angular deprecation', () => { describe('Angular deprecation', () => {
const deprecationText = /legacy platform based on AngularJS/i; const deprecationText = /legacy platform based on AngularJS/i;
const oldAngularDetected = mockDS.angularDetected; const oldAngularDetected = mockDS.meta.angular?.detected ?? false;
const oldDatasources = config.datasources; const oldDatasources = config.datasources;
afterEach(() => { afterEach(() => {
mockDS.angularDetected = oldAngularDetected; mockDS.meta.angular = { detected: oldAngularDetected, hideDeprecation: false };
config.datasources = oldDatasources; config.datasources = oldDatasources;
}); });
it('Should render angular deprecation notice for angular plugins', async () => { 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; config.datasources[mockDS.name] = mockDS;
renderScenario({}); renderScenario({});
await waitFor(async () => { await waitFor(async () => {
@@ -145,7 +145,7 @@ describe('QueryGroup', () => {
}); });
it('Should not render angular deprecation notice for non-angular plugins', async () => { 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; config.datasources[mockDS.name] = mockDS;
renderScenario({}); renderScenario({});
await waitFor(async () => { await waitFor(async () => {

View File

@@ -27,7 +27,7 @@ import { DashboardQueryEditor, isSharedDashboardQuery } from 'app/plugins/dataso
import { GrafanaQuery } from 'app/plugins/datasource/grafana/types'; import { GrafanaQuery } from 'app/plugins/datasource/grafana/types';
import { QueryGroupOptions } from 'app/types'; import { QueryGroupOptions } from 'app/types';
import { isAngularDatasourcePlugin } from '../../plugins/angularDeprecation/utils'; import { isAngularDatasourcePluginAndNotHidden } from '../../plugins/angularDeprecation/utils';
import { PanelQueryRunner } from '../state/PanelQueryRunner'; import { PanelQueryRunner } from '../state/PanelQueryRunner';
import { updateQueries } from '../state/updateQueries'; import { updateQueries } from '../state/updateQueries';
@@ -255,7 +255,7 @@ export class QueryGroup extends PureComponent<Props, State> {
</> </>
)} )}
</div> </div>
{dataSource && isAngularDatasourcePlugin(dataSource.uid) && ( {dataSource && isAngularDatasourcePluginAndNotHidden(dataSource.uid) && (
<AngularDeprecationPluginNotice <AngularDeprecationPluginNotice
pluginId={dataSource.type} pluginId={dataSource.type}
pluginType={PluginType.datasource} pluginType={PluginType.datasource}