Angular: Option to disable angular support and isolate angular dependencies (#45421)

* Angular: Initial setting that disables angular, load angular support in separate chunk

* Load angular panels on demand

* Load alerting in separate chunk only when angularSupportEnabled

* progress, do not export core_module if angular disabled

* Progress

* Update public/app/features/plugins/built_in_plugins.ts

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>

* Removing remaining usage of angular from outside angular app (not counting plugins)

* Update config and docs

* Fix sample.ini

* Update public/app/features/alerting/AlertTab.tsx

Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>

* Fixing prettier issue

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
This commit is contained in:
Torkel Ödegaard
2022-02-16 17:14:33 +01:00
committed by GitHub
parent 8bb3de3037
commit 2b9e46d1f8
24 changed files with 118 additions and 94 deletions

View File

@@ -292,6 +292,9 @@ content_security_policy = false
# $ROOT_PATH is server.root_url without the protocol.
content_security_policy_template = """script-src 'self' 'unsafe-eval' 'unsafe-inline' 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline' blob:;img-src * data:;base-uri 'self';connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;manifest-src 'self';media-src 'none';form-action 'self';"""
# Controls if old angular plugins are supported or not. This will be disabled by default in Grafana v9.
angular_support_enabled = true
#################################### Snapshots ###########################
[snapshots]
# snapshot sharing options

View File

@@ -292,6 +292,9 @@
# $ROOT_PATH is server.root_url without the protocol.
;content_security_policy_template = """script-src 'self' 'unsafe-eval' 'unsafe-inline' 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline' blob:;img-src * data:;base-uri 'self';connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;manifest-src 'self';media-src 'none';form-action 'self';"""
# Controls if old angular plugins are supported or not. This will be disabled by default in Grafana v9.
;angular_support_enabled = true
#################################### Snapshots ###########################
[snapshots]
# snapshot sharing options

View File

@@ -582,6 +582,21 @@ Set Content Security Policy template used when adding the Content-Security-Polic
<hr />
### angular_support_enabled
This currently defaults to `true` but will in Grafana v9 default to `false`. When set to false the angular framework and support components will not be loaded. This means that
all plugins and core features that depend on angular support will stop working.
Current core features that will stop working:
- Heatmap panel
- Old graph panel
- Old table panel
- Postgres, MySQL and MSSQL data source query editors
- Legacy alerting edit rule UI
Before we disable angular support by default we plan to migrate these remaining areas to React.
## [snapshots]
### external_enabled

View File

@@ -138,4 +138,5 @@ export interface GrafanaConfig {
geomapDefaultBaseLayer?: MapLayerOptions;
geomapDisableCustomBaseLayer?: boolean;
unifiedAlertingEnabled: boolean;
angularSupportEnabled: boolean;
}

View File

@@ -41,6 +41,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
alertingErrorOrTimeout = '';
alertingNoDataOrNullValues = '';
alertingMinInterval = 1;
angularSupportEnabled = false;
authProxyEnabled = false;
exploreEnabled = false;
ldapEnabled = false;

View File

@@ -229,6 +229,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
"externalUserMngLinkName": setting.ExternalUserMngLinkName,
"viewersCanEdit": setting.ViewersCanEdit,
"angularSupportEnabled": hs.Cfg.AngularSupportEnabled,
"editorsCanAdmin": hs.Cfg.EditorsCanAdmin,
"disableSanitizeHtml": hs.Cfg.DisableSanitizeHtml,
"pluginsToPreload": pluginsToPreload,

View File

@@ -254,7 +254,8 @@ type Cfg struct {
// CSPEnabled toggles Content Security Policy support.
CSPEnabled bool
// CSPTemplate contains the Content Security Policy template.
CSPTemplate string
CSPTemplate string
AngularSupportEnabled bool
TempDataLifetime time.Duration
PluginsEnableAlpha bool
@@ -1191,6 +1192,7 @@ func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error {
cfg.StrictTransportSecuritySubDomains = security.Key("strict_transport_security_subdomains").MustBool(false)
cfg.CSPEnabled = security.Key("content_security_policy").MustBool(false)
cfg.CSPTemplate = security.Key("content_security_policy_template").MustString("")
cfg.AngularSupportEnabled = security.Key("angular_support_enabled").MustBool(true)
// read data source proxy whitelist
DataProxyWhiteList = make(map[string]bool)

View File

@@ -15,15 +15,16 @@ import { GrafanaRoute } from './core/navigation/GrafanaRoute';
import { AppNotificationList } from './core/components/AppNotifications/AppNotificationList';
import { SearchWrapper } from 'app/features/search';
import { LiveConnectionWarning } from './features/live/LiveConnectionWarning';
import { AngularRoot } from './angular/AngularRoot';
import { I18nProvider } from './core/localisation';
import { AngularRoot } from './angular/AngularRoot';
import { loadAndInitAngularIfEnabled } from './angular/loadAndInitAngularIfEnabled';
interface AppWrapperProps {
app: GrafanaApp;
}
interface AppWrapperState {
ngInjector: any;
ready?: boolean;
}
/** Used by enterprise */
@@ -39,27 +40,14 @@ export function addPageBanner(fn: ComponentType) {
}
export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState> {
container = React.createRef<HTMLDivElement>();
constructor(props: AppWrapperProps) {
super(props);
this.state = {
ngInjector: null,
};
this.state = {};
}
componentDidMount() {
if (this.container) {
this.bootstrapNgApp();
} else {
throw new Error('Failed to boot angular app, no container to attach to');
}
}
bootstrapNgApp() {
const injector = this.props.app.angularApp.bootstrap();
this.setState({ ngInjector: injector });
async componentDidMount() {
await loadAndInitAngularIfEnabled();
this.setState({ ready: true });
$('.preloader').remove();
}
@@ -91,6 +79,8 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
}
render() {
const { ready } = this.state;
navigationLogger('AppWrapper', false, 'rendering');
const newNavigationEnabled = Boolean(config.featureToggles.newNavigation);
@@ -111,10 +101,10 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
<Banner key={index.toString()} />
))}
<AngularRoot ref={this.container} />
<AngularRoot />
<AppNotificationList />
<SearchWrapper />
{this.state.ngInjector && this.renderRoutes()}
{ready && this.renderRoutes()}
{bodyRenderHooks.map((Hook, index) => (
<Hook key={index.toString()} />
))}

View File

@@ -14,6 +14,11 @@ import { extend } from 'lodash';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { getTemplateSrv } from '@grafana/runtime';
import { registerComponents } from './registerComponents';
import { exposeToPlugin } from 'app/features/plugins/plugin_loader';
import appEvents from 'app/core/app_events';
import { contextSrv } from 'app/core/services/context_srv';
import * as sdk from 'app/plugins/sdk';
import { promiseToDigest } from './promiseToDigest';
export class AngularApp {
ngModuleDependencies: any[];
@@ -93,6 +98,18 @@ export class AngularApp {
registerComponents();
initAngularRoutingBridge();
// Angular plugins import this
exposeToPlugin('angular', angular);
exposeToPlugin('app/core/utils/promiseToDigest', { promiseToDigest, __esModule: true });
exposeToPlugin('app/plugins/sdk', sdk);
exposeToPlugin('app/core/core_module', coreModule);
exposeToPlugin('app/core/core', {
coreModule: coreModule,
appEvents: appEvents,
contextSrv: contextSrv,
__esModule: true,
});
// disable tool tip animation
$.fn.tooltip.defaults.animation = false;
}

View File

@@ -9,6 +9,7 @@ import './services/popover_srv';
import './services/timer';
import './services/AngularLoader';
import '../angular/jquery_extended';
import './dropdown_typeahead';
import './autofill_event_fix';
import './metric_segment';
@@ -37,3 +38,4 @@ import './components/plugin_component';
import './GrafanaCtrl';
export { AngularApp } from './AngularApp';
export { coreModule } from './core_module';

View File

@@ -0,0 +1,22 @@
import { config, setAngularLoader } from '@grafana/runtime';
export async function loadAndInitAngularIfEnabled() {
if (config.angularSupportEnabled) {
const { AngularApp } = await import(/* webpackChunkName: "AngularApp" */ './index');
const app = new AngularApp();
app.init();
app.bootstrap();
} else {
setAngularLoader({
load: (elem, scopeProps, template) => {
return {
destroy: () => {},
digest: () => {},
getScope: () => {
return {};
},
};
},
});
}
}

View File

@@ -62,7 +62,6 @@ import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer'
import { PanelDataErrorView } from './features/panel/components/PanelDataErrorView';
import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelDataErrorView';
import { DatasourceSrv } from './features/plugins/datasource_srv';
import { AngularApp } from './angular';
import { ModalManager } from './core/services/ModalManager';
import { initWindowRuntime } from './features/runtime/init';
import { createQueryVariableAdapter } from './features/variables/query/adapter';
@@ -89,12 +88,6 @@ if (process.env.NODE_ENV === 'development') {
}
export class GrafanaApp {
angularApp: AngularApp;
constructor() {
this.angularApp = new AngularApp();
}
async init() {
try {
setBackendSrv(backendSrv);
@@ -148,9 +141,6 @@ export class GrafanaApp {
const modalManager = new ModalManager();
modalManager.init();
// Init angular
this.angularApp.init();
// Preload selected app plugins
await preloadPlugins(config.pluginsToPreload);

View File

@@ -1,5 +1,3 @@
import './jquery_extended';
import './services/search_srv';
import { colors, JsonExplorer } from '@grafana/ui/';
import appEvents from './app_events';
import { assignModelProperties } from './utils/model_utils';

View File

@@ -2,13 +2,10 @@ import React, { PureComponent } from 'react';
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import { Alert, Button, ConfirmModal, Container, CustomScrollbar, HorizontalGroup, IconName, Modal } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import { AngularComponent, getAngularLoader, getDataSourceSrv } from '@grafana/runtime';
import { AngularComponent, config, getAngularLoader, getDataSourceSrv } from '@grafana/runtime';
import { getAlertingValidationMessage } from './getAlertingValidationMessage';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import StateHistory from './StateHistory';
import 'app/features/alerting/AlertTabCtrl';
import { DashboardModel } from '../dashboard/state/DashboardModel';
import { PanelModel } from '../dashboard/state/PanelModel';
import { TestRuleResult } from './TestRuleResult';
@@ -57,8 +54,14 @@ class UnConnectedAlertTab extends PureComponent<Props, State> {
showTestRule: false,
};
componentDidMount() {
this.loadAlertTab();
async componentDidMount() {
if (config.angularSupportEnabled) {
await import(/* webpackChunkName: "AlertTabCtrl" */ 'app/features/alerting/AlertTabCtrl');
this.loadAlertTab();
} else {
// TODO probably need to migrate AlertTab to react
alert('Angular support disabled, legacy alerting cannot function without angular support');
}
}
onAngularPanelUpdated = () => {

View File

@@ -1,4 +1,3 @@
import { isArray } from 'angular';
import {
AlertManagerCortexConfig,
GrafanaManagedReceiverConfig,
@@ -6,6 +5,7 @@ import {
Route,
} from 'app/plugins/datasource/alertmanager/types';
import { CloudNotifierType, NotifierDTO, NotifierType } from 'app/types';
import { isArray } from 'lodash';
import {
CloudChannelConfig,
CloudChannelMap,

View File

@@ -5,7 +5,6 @@ import React, { useEffect, useState } from 'react';
import { Prompt } from 'react-router-dom';
import { DashboardModel } from '../../state/DashboardModel';
import { each, filter, find } from 'lodash';
import angular from 'angular';
import { UnsavedChangesModal } from '../SaveDashboard/UnsavedChangesModal';
import * as H from 'history';
import { SaveLibraryPanelModal } from 'app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal';
@@ -248,8 +247,8 @@ export function hasChanges(current: DashboardModel, original: any) {
currentTimepicker.now = originalTimepicker.now;
}
const currentJson = angular.toJson(currentClean);
const originalJson = angular.toJson(originalClean);
const currentJson = JSON.stringify(currentClean, null);
const originalJson = JSON.stringify(originalClean, null);
return currentJson !== originalJson;
}

View File

@@ -45,15 +45,12 @@ import * as timeseriesPanel from 'app/plugins/panel/timeseries/module';
import * as stateTimelinePanel from 'app/plugins/panel/state-timeline/module';
import * as statusHistoryPanel from 'app/plugins/panel/status-history/module';
import * as candlestickPanel from 'app/plugins/panel/candlestick/module';
import * as graphPanel from 'app/plugins/panel/graph/module';
import * as xyChartPanel from 'app/plugins/panel/xychart/module';
import * as dashListPanel from 'app/plugins/panel/dashlist/module';
import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module';
import * as alertListPanel from 'app/plugins/panel/alertlist/module';
import * as annoListPanel from 'app/plugins/panel/annolist/module';
import * as heatmapPanel from 'app/plugins/panel/heatmap/module';
import * as tablePanel from 'app/plugins/panel/table/module';
import * as oldTablePanel from 'app/plugins/panel/table-old/module';
import * as statPanel from 'app/plugins/panel/stat/module';
import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
import * as gaugePanel from 'app/plugins/panel/gauge/module';
@@ -73,6 +70,11 @@ import * as alertGroupsPanel from 'app/plugins/panel/alertGroups/module';
const geomapPanel = async () => await import(/* webpackChunkName: "geomapPanel" */ 'app/plugins/panel/geomap/module');
const canvasPanel = async () => await import(/* webpackChunkName: "canvasPanel" */ 'app/plugins/panel/canvas/module');
const iconPanel = async () => await import(/* webpackChunkName: "iconPanel" */ 'app/plugins/panel/icon/module');
const graphPanel = async () => await import(/* webpackChunkName: "graphPlugin" */ 'app/plugins/panel/graph/module');
const heatmapPanel = async () =>
await import(/* webpackChunkName: "heatmapPlugin" */ 'app/plugins/panel/heatmap/module');
const tableOldPanel = async () =>
await import(/* webpackChunkName: "tableOldPlugin" */ 'app/plugins/panel/table-old/module');
const builtInPlugins: any = {
'app/plugins/datasource/graphite/module': graphitePlugin,
@@ -112,7 +114,7 @@ const builtInPlugins: any = {
'app/plugins/panel/annolist/module': annoListPanel,
'app/plugins/panel/heatmap/module': heatmapPanel,
'app/plugins/panel/table/module': tablePanel,
'app/plugins/panel/table-old/module': oldTablePanel,
'app/plugins/panel/table-old/module': tableOldPanel,
'app/plugins/panel/news/module': newsPanel,
'app/plugins/panel/live/module': livePanel,
'app/plugins/panel/stat/module': statPanel,

View File

@@ -28,25 +28,22 @@ export function importPanelPluginFromMeta(meta: grafanaData.PanelPluginMeta): Pr
return getPanelPlugin(meta);
}
function getPanelPlugin(meta: grafanaData.PanelPluginMeta): Promise<grafanaData.PanelPlugin> {
return importPluginModule(meta.module, meta.info?.version)
.then((pluginExports) => {
if (pluginExports.plugin) {
return pluginExports.plugin as grafanaData.PanelPlugin;
} else if (pluginExports.PanelCtrl) {
const plugin = new grafanaData.PanelPlugin(null);
plugin.angularPanelCtrl = pluginExports.PanelCtrl;
return plugin;
}
throw new Error('missing export: plugin or PanelCtrl');
})
.then((plugin) => {
plugin.meta = meta;
return plugin;
})
.catch((err) => {
// TODO, maybe a different error plugin
console.warn('Error loading panel plugin: ' + meta.id, err);
return getPanelPluginLoadError(meta, err);
});
async function getPanelPlugin(meta: grafanaData.PanelPluginMeta): Promise<grafanaData.PanelPlugin> {
try {
const pluginExports = await importPluginModule(meta.module, meta.info?.version);
let plugin = pluginExports.plugin;
if (!plugin && pluginExports.PanelCtrl) {
plugin = new grafanaData.PanelPlugin(null);
plugin.angularPanelCtrl = pluginExports.PanelCtrl;
}
plugin.meta = meta;
return plugin;
} catch (err) {
// TODO, maybe a different error plugin
console.warn('Error loading panel plugin: ' + meta.id, err);
return getPanelPluginLoadError(meta, err);
}
}

View File

@@ -1,9 +1,7 @@
// eslint-disable-next-line lodash/import-scope
import _ from 'lodash';
import * as sdk from 'app/plugins/sdk';
import kbn from 'app/core/utils/kbn';
import moment from 'moment'; // eslint-disable-line no-restricted-imports
import angular from 'angular';
import jquery from 'jquery';
// Experimental module exports
@@ -21,12 +19,10 @@ import * as redux from 'redux';
import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2';
import TableModel from 'app/core/table_model';
import { coreModule } from 'app/angular/core_module';
import { appEvents, contextSrv } from 'app/core/core';
import * as flatten from 'app/core/utils/flatten';
import * as ticks from 'app/core/utils/ticks';
import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv';
import { promiseToDigest } from 'app/angular/promiseToDigest';
import impressionSrv from 'app/core/services/impression_srv';
import builtInPlugins from './built_in_plugins';
import * as d3 from 'd3';
@@ -75,7 +71,7 @@ grafanaRuntime.SystemJS.config({
},
});
function exposeToPlugin(name: string, component: any) {
export function exposeToPlugin(name: string, component: any) {
grafanaRuntime.SystemJS.registerDynamic(name, [], true, (require: any, exports: any, module: { exports: any }) => {
module.exports = component;
});
@@ -87,7 +83,6 @@ exposeToPlugin('@grafana/runtime', grafanaRuntime);
exposeToPlugin('lodash', _);
exposeToPlugin('moment', moment);
exposeToPlugin('jquery', jquery);
exposeToPlugin('angular', angular);
exposeToPlugin('d3', d3);
exposeToPlugin('rxjs', rxjs);
exposeToPlugin('rxjs/operators', rxjsOperators);
@@ -120,24 +115,16 @@ exposeToPlugin('app/core/services/backend_srv', {
getBackendSrv,
});
exposeToPlugin('app/plugins/sdk', sdk);
exposeToPlugin('app/core/utils/datemath', grafanaData.dateMath);
exposeToPlugin('app/core/utils/flatten', flatten);
exposeToPlugin('app/core/utils/kbn', kbn);
exposeToPlugin('app/core/utils/ticks', ticks);
exposeToPlugin('app/core/utils/promiseToDigest', {
promiseToDigest: promiseToDigest,
__esModule: true,
});
exposeToPlugin('app/core/config', config);
exposeToPlugin('app/core/time_series', TimeSeries);
exposeToPlugin('app/core/time_series2', TimeSeries);
exposeToPlugin('app/core/table_model', TableModel);
exposeToPlugin('app/core/app_events', appEvents);
exposeToPlugin('app/core/core_module', coreModule);
exposeToPlugin('app/core/core', {
coreModule: coreModule,
appEvents: appEvents,
contextSrv: contextSrv,
__esModule: true,

View File

@@ -1,4 +1,3 @@
import angular from 'angular';
import { castArray, isEqual } from 'lodash';
import {
DataQuery,
@@ -601,7 +600,7 @@ const timeRangeUpdated =
const updatedVariable = getVariable<VariableWithOptions>(identifier.id, getState());
const updatedOptions = updatedVariable.options;
if (angular.toJson(previousOptions) !== angular.toJson(updatedOptions)) {
if (JSON.stringify(previousOptions) !== JSON.stringify(updatedOptions)) {
const dashboard = getState().dashboard.getModel();
dashboard?.templateVariableValueUpdated();
}

View File

@@ -1,5 +1,3 @@
import { auto } from 'angular';
export class QueryCtrl {
target: any;
datasource: any;
@@ -8,7 +6,7 @@ export class QueryCtrl {
hasRawMode = false;
error = '';
constructor(public $scope: any, _$injector: auto.IInjectorService) {
constructor(public $scope: any) {
this.panelCtrl = this.panelCtrl || { panel: {} };
this.target = this.target || { target: '' };
this.panel = this.panelCtrl.panel;

View File

@@ -1,5 +0,0 @@
import { IScope } from 'angular';
export interface Scope extends IScope {
[key: string]: any;
}

View File

@@ -15,7 +15,6 @@ export * from './explore';
export * from './store';
export * from './ldap';
export * from './appEvent';
export * from './angular';
export * from './query';
export * from './preferences';
export * from './accessControl';