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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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 */
rawRef?: DataSourceRef;
angularDetected?: boolean;
}
/**

View File

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

View File

@ -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 {

View File

@ -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 {

View File

@ -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},
},
},
},

View File

@ -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() {

View File

@ -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
}

View File

@ -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 (

View File

@ -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

View File

@ -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{

View File

@ -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,
}
}

View File

@ -102,7 +102,7 @@ export const OptionsPaneOptions = (props: OptionPaneRenderProps) => {
return (
<div className={styles.wrapper}>
<div className={styles.formBox}>
{panel.isAngularPlugin() && (
{panel.isAngularPlugin() && !plugin.meta.angular?.hideDeprecation && (
<AngularDeprecationPluginNotice
className={styles.angularDeprecationWrapper}
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 { contextSrv } from 'app/core/services/context_srv';
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 { onTimeRangeUpdated } from 'app/features/variables/state/actions';
import { GetVariables, getVariablesByKey } from 'app/features/variables/state/selectors';
@ -1293,10 +1293,15 @@ export class DashboardModel implements TimeModel {
}
hasAngularPlugins(): boolean {
return this.panels.some(
(panel) =>
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;
});
}
}

View File

@ -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() {

View File

@ -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);

View File

@ -1,7 +1,16 @@
import { DataSourceInstanceSettings } from '@grafana/data';
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 {
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;
}

View File

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

View File

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

View File

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

View File

@ -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 () => {

View File

@ -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<Props, State> {
</>
)}
</div>
{dataSource && isAngularDatasourcePlugin(dataSource.uid) && (
{dataSource && isAngularDatasourcePluginAndNotHidden(dataSource.uid) && (
<AngularDeprecationPluginNotice
pluginId={dataSource.type}
pluginType={PluginType.datasource}