mirror of
https://github.com/grafana/grafana.git
synced 2025-07-28 19:42:44 +08:00
Plugins: Introduce LoadingStrategy
for frontend loading logic (#92392)
* do it all * feat(plugins): move loadingStrategy to ds pluginMeta and add to plugin settings endpoint * support child plugins and update tests * use relative path for nested plugins * feat(plugins): support nested plugins in the plugin loader cache by extracting pluginId from path * feat(grafana-data): add plugin loading strategy to plugin meta and export * feat(plugins): pass down loadingStrategy to fe plugin loader * refactor(plugins): make PluginLoadingStrategy an enum * feat(plugins): add the loading strategy to the fe plugin loader cache * feat(plugins): load fe plugin js assets as script tags based on be loadingStrategy * add more tests * feat(plugins): add loading strategy to plugin preloader * feat(plugins): make loadingStrategy a maybe and provide fetch fallback * test(alerting): update config.apps mocks to include loadingStrategy * fix format --------- Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
This commit is contained in:
@ -579,6 +579,7 @@ export {
|
|||||||
PluginSignatureType,
|
PluginSignatureType,
|
||||||
PluginErrorCode,
|
PluginErrorCode,
|
||||||
PluginIncludeType,
|
PluginIncludeType,
|
||||||
|
PluginLoadingStrategy,
|
||||||
GrafanaPlugin,
|
GrafanaPlugin,
|
||||||
type PluginError,
|
type PluginError,
|
||||||
type AngularMeta,
|
type AngularMeta,
|
||||||
|
@ -59,6 +59,12 @@ export interface AngularMeta {
|
|||||||
hideDeprecation: boolean;
|
hideDeprecation: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Signals to SystemJS how to load frontend js assets.
|
||||||
|
export enum PluginLoadingStrategy {
|
||||||
|
fetch = 'fetch',
|
||||||
|
script = 'script',
|
||||||
|
}
|
||||||
|
|
||||||
export interface PluginMeta<T extends KeyValue = {}> {
|
export interface PluginMeta<T extends KeyValue = {}> {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -91,6 +97,7 @@ export interface PluginMeta<T extends KeyValue = {}> {
|
|||||||
live?: boolean;
|
live?: boolean;
|
||||||
angular?: AngularMeta;
|
angular?: AngularMeta;
|
||||||
angularDetected?: boolean;
|
angularDetected?: boolean;
|
||||||
|
loadingStrategy?: PluginLoadingStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PluginDependencyInfo {
|
interface PluginDependencyInfo {
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
SystemDateFormatSettings,
|
SystemDateFormatSettings,
|
||||||
getThemeById,
|
getThemeById,
|
||||||
AngularMeta,
|
AngularMeta,
|
||||||
|
PluginLoadingStrategy,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
export interface AzureSettings {
|
export interface AzureSettings {
|
||||||
@ -40,6 +41,7 @@ export type AppPluginConfig = {
|
|||||||
version: string;
|
version: string;
|
||||||
preload: boolean;
|
preload: boolean;
|
||||||
angular: AngularMeta;
|
angular: AngularMeta;
|
||||||
|
loadingStrategy: PluginLoadingStrategy;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PreinstalledPlugin = {
|
export type PreinstalledPlugin = {
|
||||||
|
@ -28,6 +28,7 @@ type PluginSetting struct {
|
|||||||
SignatureType plugins.SignatureType `json:"signatureType"`
|
SignatureType plugins.SignatureType `json:"signatureType"`
|
||||||
SignatureOrg string `json:"signatureOrg"`
|
SignatureOrg string `json:"signatureOrg"`
|
||||||
AngularDetected bool `json:"angularDetected"`
|
AngularDetected bool `json:"angularDetected"`
|
||||||
|
LoadingStrategy plugins.LoadingStrategy `json:"loadingStrategy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginListItem struct {
|
type PluginListItem struct {
|
||||||
|
@ -110,7 +110,8 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
|||||||
|
|
||||||
apps := make(map[string]*plugins.AppDTO, 0)
|
apps := make(map[string]*plugins.AppDTO, 0)
|
||||||
for _, ap := range availablePlugins[plugins.TypeApp] {
|
for _, ap := range availablePlugins[plugins.TypeApp] {
|
||||||
apps[ap.Plugin.ID] = newAppDTO(
|
apps[ap.Plugin.ID] = hs.newAppDTO(
|
||||||
|
c.Req.Context(),
|
||||||
ap.Plugin,
|
ap.Plugin,
|
||||||
ap.Settings,
|
ap.Settings,
|
||||||
)
|
)
|
||||||
@ -140,18 +141,19 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
|||||||
}
|
}
|
||||||
|
|
||||||
panels[panel.ID] = plugins.PanelDTO{
|
panels[panel.ID] = plugins.PanelDTO{
|
||||||
ID: panel.ID,
|
ID: panel.ID,
|
||||||
Name: panel.Name,
|
Name: panel.Name,
|
||||||
AliasIDs: panel.AliasIDs,
|
AliasIDs: panel.AliasIDs,
|
||||||
Info: panel.Info,
|
Info: panel.Info,
|
||||||
Module: panel.Module,
|
Module: panel.Module,
|
||||||
BaseURL: panel.BaseURL,
|
BaseURL: panel.BaseURL,
|
||||||
SkipDataQuery: panel.SkipDataQuery,
|
SkipDataQuery: panel.SkipDataQuery,
|
||||||
HideFromList: panel.HideFromList,
|
HideFromList: panel.HideFromList,
|
||||||
ReleaseState: string(panel.State),
|
ReleaseState: string(panel.State),
|
||||||
Signature: string(panel.Signature),
|
Signature: string(panel.Signature),
|
||||||
Sort: getPanelSort(panel.ID),
|
Sort: getPanelSort(panel.ID),
|
||||||
Angular: panel.Angular,
|
Angular: panel.Angular,
|
||||||
|
LoadingStrategy: hs.pluginAssets.LoadingStrategy(c.Req.Context(), panel),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -455,6 +457,7 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug
|
|||||||
BaseURL: plugin.BaseURL,
|
BaseURL: plugin.BaseURL,
|
||||||
Angular: plugin.Angular,
|
Angular: plugin.Angular,
|
||||||
MultiValueFilterOperators: plugin.MultiValueFilterOperators,
|
MultiValueFilterOperators: plugin.MultiValueFilterOperators,
|
||||||
|
LoadingStrategy: hs.pluginAssets.LoadingStrategy(c.Req.Context(), plugin),
|
||||||
}
|
}
|
||||||
|
|
||||||
if ds.JsonData == nil {
|
if ds.JsonData == nil {
|
||||||
@ -551,13 +554,14 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug
|
|||||||
return dataSources, nil
|
return dataSources, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAppDTO(plugin pluginstore.Plugin, settings pluginsettings.InfoDTO) *plugins.AppDTO {
|
func (hs *HTTPServer) newAppDTO(ctx context.Context, plugin pluginstore.Plugin, settings pluginsettings.InfoDTO) *plugins.AppDTO {
|
||||||
app := &plugins.AppDTO{
|
app := &plugins.AppDTO{
|
||||||
ID: plugin.ID,
|
ID: plugin.ID,
|
||||||
Version: plugin.Info.Version,
|
Version: plugin.Info.Version,
|
||||||
Path: plugin.Module,
|
Path: plugin.Module,
|
||||||
Preload: false,
|
Preload: false,
|
||||||
Angular: plugin.Angular,
|
Angular: plugin.Angular,
|
||||||
|
LoadingStrategy: hs.pluginAssets.LoadingStrategy(ctx, plugin),
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.Enabled {
|
if settings.Enabled {
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/licensing"
|
"github.com/grafana/grafana/pkg/services/licensing"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||||
"github.com/grafana/grafana/pkg/services/rendering"
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
@ -35,7 +36,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.FeatureToggles, pstore pluginstore.Store, psettings pluginsettings.Service) (*web.Mux, *HTTPServer) {
|
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.FeatureToggles, pstore pluginstore.Store, psettings pluginsettings.Service, passets *pluginassets.Service) (*web.Mux, *HTTPServer) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
db.InitTestDB(t)
|
db.InitTestDB(t)
|
||||||
// nolint:staticcheck
|
// nolint:staticcheck
|
||||||
@ -50,6 +51,11 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pluginsCDN := pluginscdn.ProvideService(&config.PluginManagementCfg{
|
||||||
|
PluginsCDNURLTemplate: cfg.PluginsCDNURLTemplate,
|
||||||
|
PluginSettings: cfg.PluginSettings,
|
||||||
|
})
|
||||||
|
|
||||||
var pluginStore = pstore
|
var pluginStore = pstore
|
||||||
if pluginStore == nil {
|
if pluginStore == nil {
|
||||||
pluginStore = &pluginstore.FakePluginStore{}
|
pluginStore = &pluginstore.FakePluginStore{}
|
||||||
@ -60,6 +66,11 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F
|
|||||||
pluginsSettings = &pluginsettings.FakePluginSettings{}
|
pluginsSettings = &pluginsettings.FakePluginSettings{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pluginsAssets = passets
|
||||||
|
if pluginsAssets == nil {
|
||||||
|
pluginsAssets = pluginassets.ProvideService(cfg, pluginsCDN)
|
||||||
|
}
|
||||||
|
|
||||||
hs := &HTTPServer{
|
hs := &HTTPServer{
|
||||||
authnService: &authntest.FakeService{},
|
authnService: &authntest.FakeService{},
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
@ -69,16 +80,14 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F
|
|||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
RendererPluginManager: &fakeRendererPluginManager{},
|
RendererPluginManager: &fakeRendererPluginManager{},
|
||||||
},
|
},
|
||||||
SQLStore: db.InitTestDB(t),
|
SQLStore: db.InitTestDB(t),
|
||||||
SettingsProvider: setting.ProvideProvider(cfg),
|
SettingsProvider: setting.ProvideProvider(cfg),
|
||||||
pluginStore: pluginStore,
|
pluginStore: pluginStore,
|
||||||
grafanaUpdateChecker: &updatechecker.GrafanaService{},
|
grafanaUpdateChecker: &updatechecker.GrafanaService{},
|
||||||
AccessControl: accesscontrolmock.New(),
|
AccessControl: accesscontrolmock.New(),
|
||||||
PluginSettings: pluginsSettings,
|
PluginSettings: pluginsSettings,
|
||||||
pluginsCDNService: pluginscdn.ProvideService(&config.PluginManagementCfg{
|
pluginsCDNService: pluginsCDN,
|
||||||
PluginsCDNURLTemplate: cfg.PluginsCDNURLTemplate,
|
pluginAssets: pluginsAssets,
|
||||||
PluginSettings: cfg.PluginSettings,
|
|
||||||
}),
|
|
||||||
namespacer: request.GetNamespaceMapper(cfg),
|
namespacer: request.GetNamespaceMapper(cfg),
|
||||||
SocialService: socialimpl.ProvideService(cfg, features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeCacheStorage(), nil, &ssosettingstests.MockService{}),
|
SocialService: socialimpl.ProvideService(cfg, features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeCacheStorage(), nil, &ssosettingstests.MockService{}),
|
||||||
managedPluginsService: managedplugins.NewNoop(),
|
managedPluginsService: managedplugins.NewNoop(),
|
||||||
@ -108,7 +117,7 @@ func TestHTTPServer_GetFrontendSettings_hideVersionAnonymous(t *testing.T) {
|
|||||||
cfg.BuildVersion = "7.8.9"
|
cfg.BuildVersion = "7.8.9"
|
||||||
cfg.BuildCommit = "01234567"
|
cfg.BuildCommit = "01234567"
|
||||||
|
|
||||||
m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), nil, nil)
|
m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), nil, nil, nil)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
|
||||||
|
|
||||||
@ -198,7 +207,7 @@ func TestHTTPServer_GetFrontendSettings_pluginsCDNBaseURL(t *testing.T) {
|
|||||||
if test.mutateCfg != nil {
|
if test.mutateCfg != nil {
|
||||||
test.mutateCfg(cfg)
|
test.mutateCfg(cfg)
|
||||||
}
|
}
|
||||||
m, _ := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), nil, nil)
|
m, _ := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), nil, nil, nil)
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
@ -221,6 +230,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
|||||||
desc string
|
desc string
|
||||||
pluginStore func() pluginstore.Store
|
pluginStore func() pluginstore.Store
|
||||||
pluginSettings func() pluginsettings.Service
|
pluginSettings func() pluginsettings.Service
|
||||||
|
pluginAssets func() *pluginassets.Service
|
||||||
expected settings
|
expected settings
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@ -245,13 +255,17 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
|||||||
Plugins: newAppSettings("test-app", false),
|
Plugins: newAppSettings("test-app", false),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
pluginAssets: func() *pluginassets.Service {
|
||||||
|
return pluginassets.ProvideService(setting.NewCfg(), pluginscdn.ProvideService(&config.PluginManagementCfg{}))
|
||||||
|
},
|
||||||
expected: settings{
|
expected: settings{
|
||||||
Apps: map[string]*plugins.AppDTO{
|
Apps: map[string]*plugins.AppDTO{
|
||||||
"test-app": {
|
"test-app": {
|
||||||
ID: "test-app",
|
ID: "test-app",
|
||||||
Preload: false,
|
Preload: false,
|
||||||
Path: "/test-app/module.js",
|
Path: "/test-app/module.js",
|
||||||
Version: "0.5.0",
|
Version: "0.5.0",
|
||||||
|
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -278,13 +292,17 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
|||||||
Plugins: newAppSettings("test-app", true),
|
Plugins: newAppSettings("test-app", true),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
pluginAssets: func() *pluginassets.Service {
|
||||||
|
return pluginassets.ProvideService(setting.NewCfg(), pluginscdn.ProvideService(&config.PluginManagementCfg{}))
|
||||||
|
},
|
||||||
expected: settings{
|
expected: settings{
|
||||||
Apps: map[string]*plugins.AppDTO{
|
Apps: map[string]*plugins.AppDTO{
|
||||||
"test-app": {
|
"test-app": {
|
||||||
ID: "test-app",
|
ID: "test-app",
|
||||||
Preload: true,
|
Preload: true,
|
||||||
Path: "/test-app/module.js",
|
Path: "/test-app/module.js",
|
||||||
Version: "0.5.0",
|
Version: "0.5.0",
|
||||||
|
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -312,14 +330,99 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
|||||||
Plugins: newAppSettings("test-app", true),
|
Plugins: newAppSettings("test-app", true),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
pluginAssets: func() *pluginassets.Service {
|
||||||
|
return pluginassets.ProvideService(setting.NewCfg(), pluginscdn.ProvideService(&config.PluginManagementCfg{}))
|
||||||
|
},
|
||||||
expected: settings{
|
expected: settings{
|
||||||
Apps: map[string]*plugins.AppDTO{
|
Apps: map[string]*plugins.AppDTO{
|
||||||
"test-app": {
|
"test-app": {
|
||||||
ID: "test-app",
|
ID: "test-app",
|
||||||
Preload: true,
|
Preload: true,
|
||||||
Path: "/test-app/module.js",
|
Path: "/test-app/module.js",
|
||||||
Version: "0.5.0",
|
Version: "0.5.0",
|
||||||
Angular: plugins.AngularMeta{Detected: true},
|
Angular: plugins.AngularMeta{Detected: true},
|
||||||
|
LoadingStrategy: plugins.LoadingStrategyFetch,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "app plugin with create plugin version compatible with script loading strategy",
|
||||||
|
pluginStore: func() pluginstore.Store {
|
||||||
|
return &pluginstore.FakePluginStore{
|
||||||
|
PluginList: []pluginstore.Plugin{
|
||||||
|
{
|
||||||
|
Module: fmt.Sprintf("/%s/module.js", "test-app"),
|
||||||
|
JSONData: plugins.JSONData{
|
||||||
|
ID: "test-app",
|
||||||
|
Info: plugins.Info{Version: "0.5.0"},
|
||||||
|
Type: plugins.TypeApp,
|
||||||
|
Preload: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pluginSettings: func() pluginsettings.Service {
|
||||||
|
return &pluginsettings.FakePluginSettings{
|
||||||
|
Plugins: newAppSettings("test-app", true),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pluginAssets: func() *pluginassets.Service {
|
||||||
|
return pluginassets.ProvideService(&setting.Cfg{
|
||||||
|
PluginSettings: map[string]map[string]string{
|
||||||
|
"test-app": {
|
||||||
|
pluginassets.CreatePluginVersionCfgKey: pluginassets.CreatePluginVersionScriptSupportEnabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, pluginscdn.ProvideService(&config.PluginManagementCfg{}))
|
||||||
|
},
|
||||||
|
expected: settings{
|
||||||
|
Apps: map[string]*plugins.AppDTO{
|
||||||
|
"test-app": {
|
||||||
|
ID: "test-app",
|
||||||
|
Preload: true,
|
||||||
|
Path: "/test-app/module.js",
|
||||||
|
Version: "0.5.0",
|
||||||
|
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "app plugin with CDN class",
|
||||||
|
pluginStore: func() pluginstore.Store {
|
||||||
|
return &pluginstore.FakePluginStore{
|
||||||
|
PluginList: []pluginstore.Plugin{
|
||||||
|
{
|
||||||
|
Class: plugins.ClassCDN,
|
||||||
|
Module: fmt.Sprintf("/%s/module.js", "test-app"),
|
||||||
|
JSONData: plugins.JSONData{
|
||||||
|
ID: "test-app",
|
||||||
|
Info: plugins.Info{Version: "0.5.0"},
|
||||||
|
Type: plugins.TypeApp,
|
||||||
|
Preload: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pluginSettings: func() pluginsettings.Service {
|
||||||
|
return &pluginsettings.FakePluginSettings{
|
||||||
|
Plugins: newAppSettings("test-app", true),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pluginAssets: func() *pluginassets.Service {
|
||||||
|
return pluginassets.ProvideService(setting.NewCfg(), pluginscdn.ProvideService(&config.PluginManagementCfg{}))
|
||||||
|
},
|
||||||
|
expected: settings{
|
||||||
|
Apps: map[string]*plugins.AppDTO{
|
||||||
|
"test-app": {
|
||||||
|
ID: "test-app",
|
||||||
|
Preload: true,
|
||||||
|
Path: "/test-app/module.js",
|
||||||
|
Version: "0.5.0",
|
||||||
|
LoadingStrategy: plugins.LoadingStrategyFetch,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -329,7 +432,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
cfg := setting.NewCfg()
|
cfg := setting.NewCfg()
|
||||||
m, _ := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), test.pluginStore(), test.pluginSettings())
|
m, _ := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), test.pluginStore(), test.pluginSettings(), test.pluginAssets())
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
|
@ -78,6 +78,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/playlist"
|
"github.com/grafana/grafana/pkg/services/playlist"
|
||||||
"github.com/grafana/grafana/pkg/services/plugindashboards"
|
"github.com/grafana/grafana/pkg/services/plugindashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||||
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||||
@ -146,6 +147,7 @@ type HTTPServer struct {
|
|||||||
pluginDashboardService plugindashboards.Service
|
pluginDashboardService plugindashboards.Service
|
||||||
pluginStaticRouteResolver plugins.StaticRouteResolver
|
pluginStaticRouteResolver plugins.StaticRouteResolver
|
||||||
pluginErrorResolver plugins.ErrorResolver
|
pluginErrorResolver plugins.ErrorResolver
|
||||||
|
pluginAssets *pluginassets.Service
|
||||||
SearchService search.Service
|
SearchService search.Service
|
||||||
ShortURLService shorturls.Service
|
ShortURLService shorturls.Service
|
||||||
QueryHistoryService queryhistory.Service
|
QueryHistoryService queryhistory.Service
|
||||||
@ -247,7 +249,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
encryptionService encryption.Internal, grafanaUpdateChecker *updatechecker.GrafanaService,
|
encryptionService encryption.Internal, grafanaUpdateChecker *updatechecker.GrafanaService,
|
||||||
pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service,
|
pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service,
|
||||||
dataSourcesService datasources.DataSourceService, queryDataService query.Service, pluginFileStore plugins.FileStore,
|
dataSourcesService datasources.DataSourceService, queryDataService query.Service, pluginFileStore plugins.FileStore,
|
||||||
serviceaccountsService serviceaccounts.Service,
|
serviceaccountsService serviceaccounts.Service, pluginAssets *pluginassets.Service,
|
||||||
authInfoService login.AuthInfoService, storageService store.StorageService,
|
authInfoService login.AuthInfoService, storageService store.StorageService,
|
||||||
notificationService notifications.Service, dashboardService dashboards.DashboardService,
|
notificationService notifications.Service, dashboardService dashboards.DashboardService,
|
||||||
dashboardProvisioningService dashboards.DashboardProvisioningService, folderService folder.Service,
|
dashboardProvisioningService dashboards.DashboardProvisioningService, folderService folder.Service,
|
||||||
@ -286,6 +288,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
pluginStore: pluginStore,
|
pluginStore: pluginStore,
|
||||||
pluginStaticRouteResolver: pluginStaticRouteResolver,
|
pluginStaticRouteResolver: pluginStaticRouteResolver,
|
||||||
pluginDashboardService: pluginDashboardService,
|
pluginDashboardService: pluginDashboardService,
|
||||||
|
pluginAssets: pluginAssets,
|
||||||
pluginErrorResolver: pluginErrorResolver,
|
pluginErrorResolver: pluginErrorResolver,
|
||||||
pluginFileStore: pluginFileStore,
|
pluginFileStore: pluginFileStore,
|
||||||
grafanaUpdateChecker: grafanaUpdateChecker,
|
grafanaUpdateChecker: grafanaUpdateChecker,
|
||||||
|
@ -208,6 +208,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response.
|
|||||||
SignatureOrg: plugin.SignatureOrg,
|
SignatureOrg: plugin.SignatureOrg,
|
||||||
SecureJsonFields: map[string]bool{},
|
SecureJsonFields: map[string]bool{},
|
||||||
AngularDetected: plugin.Angular.Detected,
|
AngularDetected: plugin.Angular.Detected,
|
||||||
|
LoadingStrategy: hs.pluginAssets.LoadingStrategy(c.Req.Context(), plugin),
|
||||||
}
|
}
|
||||||
|
|
||||||
if plugin.IsApp() {
|
if plugin.IsApp() {
|
||||||
|
@ -41,6 +41,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/org/orgtest"
|
"github.com/grafana/grafana/pkg/services/org/orgtest"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||||
@ -817,6 +818,7 @@ func Test_PluginsSettings(t *testing.T) {
|
|||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
},
|
},
|
||||||
SecureJsonFields: map[string]bool{},
|
SecureJsonFields: map[string]bool{},
|
||||||
|
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -841,6 +843,8 @@ func Test_PluginsSettings(t *testing.T) {
|
|||||||
ErrorCode: tc.errCode,
|
ErrorCode: tc.errCode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
pluginCDN := pluginscdn.ProvideService(&config.PluginManagementCfg{})
|
||||||
|
hs.pluginAssets = pluginassets.ProvideService(hs.Cfg, pluginCDN)
|
||||||
hs.pluginErrorResolver = pluginerrs.ProvideStore(errTracker)
|
hs.pluginErrorResolver = pluginerrs.ProvideStore(errTracker)
|
||||||
var err error
|
var err error
|
||||||
hs.pluginsUpdateChecker, err = updatechecker.ProvidePluginsService(hs.Cfg, nil, tracing.InitializeTracerForTest())
|
hs.pluginsUpdateChecker, err = updatechecker.ProvidePluginsService(hs.Cfg, nil, tracing.InitializeTracerForTest())
|
||||||
|
@ -57,13 +57,14 @@ func (s *Service) Base(n PluginInfo) (string, error) {
|
|||||||
return s.cdn.AssetURL(n.pluginJSON.ID, n.pluginJSON.Info.Version, "")
|
return s.cdn.AssetURL(n.pluginJSON.ID, n.pluginJSON.Info.Version, "")
|
||||||
}
|
}
|
||||||
if n.parent != nil {
|
if n.parent != nil {
|
||||||
|
relPath, err := n.parent.fs.Rel(n.fs.Base())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
if s.cdn.PluginSupported(n.parent.pluginJSON.ID) {
|
if s.cdn.PluginSupported(n.parent.pluginJSON.ID) {
|
||||||
relPath, err := n.parent.fs.Rel(n.fs.Base())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return s.cdn.AssetURL(n.parent.pluginJSON.ID, n.parent.pluginJSON.Info.Version, relPath)
|
return s.cdn.AssetURL(n.parent.pluginJSON.ID, n.parent.pluginJSON.Info.Version, relPath)
|
||||||
}
|
}
|
||||||
|
return path.Join("public/plugins", n.parent.pluginJSON.ID, relPath), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return path.Join("public/plugins", n.pluginJSON.ID), nil
|
return path.Join("public/plugins", n.pluginJSON.ID), nil
|
||||||
@ -87,13 +88,14 @@ func (s *Service) Module(n PluginInfo) (string, error) {
|
|||||||
return s.cdn.AssetURL(n.pluginJSON.ID, n.pluginJSON.Info.Version, "module.js")
|
return s.cdn.AssetURL(n.pluginJSON.ID, n.pluginJSON.Info.Version, "module.js")
|
||||||
}
|
}
|
||||||
if n.parent != nil {
|
if n.parent != nil {
|
||||||
|
relPath, err := n.parent.fs.Rel(n.fs.Base())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
if s.cdn.PluginSupported(n.parent.pluginJSON.ID) {
|
if s.cdn.PluginSupported(n.parent.pluginJSON.ID) {
|
||||||
relPath, err := n.parent.fs.Rel(n.fs.Base())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return s.cdn.AssetURL(n.parent.pluginJSON.ID, n.parent.pluginJSON.Info.Version, path.Join(relPath, "module.js"))
|
return s.cdn.AssetURL(n.parent.pluginJSON.ID, n.parent.pluginJSON.Info.Version, path.Join(relPath, "module.js"))
|
||||||
}
|
}
|
||||||
|
return path.Join("public/plugins", n.parent.pluginJSON.ID, relPath, "module.js"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return path.Join("public/plugins", n.pluginJSON.ID, "module.js"), nil
|
return path.Join("public/plugins", n.pluginJSON.ID, "module.js"), nil
|
||||||
@ -117,10 +119,6 @@ func (s *Service) RelativeURL(n PluginInfo, pathStr string) (string, error) {
|
|||||||
return s.cdn.AssetURL(n.parent.pluginJSON.ID, n.parent.pluginJSON.Info.Version, path.Join(relPath, pathStr))
|
return s.cdn.AssetURL(n.parent.pluginJSON.ID, n.parent.pluginJSON.Info.Version, path.Join(relPath, pathStr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.cdn.PluginSupported(n.pluginJSON.ID) {
|
|
||||||
return s.cdn.NewCDNURLConstructor(n.pluginJSON.ID, n.pluginJSON.Info.Version).StringPath(pathStr)
|
|
||||||
}
|
|
||||||
// Local
|
// Local
|
||||||
u, err := url.Parse(pathStr)
|
u, err := url.Parse(pathStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -187,3 +187,153 @@ func TestService(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestService_ChildPlugins(t *testing.T) {
|
||||||
|
type expected struct {
|
||||||
|
module string
|
||||||
|
baseURL string
|
||||||
|
relURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
tcs := []struct {
|
||||||
|
name string
|
||||||
|
cfg *config.PluginManagementCfg
|
||||||
|
pluginInfo func() PluginInfo
|
||||||
|
expected expected
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Local FS external plugin",
|
||||||
|
cfg: &config.PluginManagementCfg{},
|
||||||
|
pluginInfo: func() PluginInfo {
|
||||||
|
return NewPluginInfo(plugins.JSONData{ID: "parent", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassExternal, plugins.NewLocalFS("/plugins/parent"), nil)
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
module: "public/plugins/parent/module.js",
|
||||||
|
baseURL: "public/plugins/parent",
|
||||||
|
relURL: "public/plugins/parent/path/to/file.txt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Local FS external plugin with child",
|
||||||
|
cfg: &config.PluginManagementCfg{},
|
||||||
|
pluginInfo: func() PluginInfo {
|
||||||
|
parentInfo := NewPluginInfo(plugins.JSONData{ID: "parent", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassExternal, plugins.NewLocalFS("/plugins/parent"), nil)
|
||||||
|
childInfo := NewPluginInfo(plugins.JSONData{ID: "child", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassExternal, plugins.NewLocalFS("/plugins/parent/child"), &parentInfo)
|
||||||
|
return childInfo
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
module: "public/plugins/parent/child/module.js",
|
||||||
|
baseURL: "public/plugins/parent/child",
|
||||||
|
relURL: "public/plugins/parent/child/path/to/file.txt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Local FS core plugin",
|
||||||
|
cfg: &config.PluginManagementCfg{},
|
||||||
|
pluginInfo: func() PluginInfo {
|
||||||
|
return NewPluginInfo(plugins.JSONData{ID: "parent", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassCore, plugins.NewLocalFS("/plugins/parent"), nil)
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
module: "core:plugin/parent",
|
||||||
|
baseURL: "public/app/plugins/parent",
|
||||||
|
relURL: "public/app/plugins/parent/path/to/file.txt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Externally-built Local FS core plugin",
|
||||||
|
cfg: &config.PluginManagementCfg{},
|
||||||
|
pluginInfo: func() PluginInfo {
|
||||||
|
return NewPluginInfo(plugins.JSONData{ID: "parent", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassCore, plugins.NewLocalFS("/plugins/parent/dist"), nil)
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
module: "public/plugins/parent/module.js",
|
||||||
|
baseURL: "public/app/plugins/parent",
|
||||||
|
relURL: "public/app/plugins/parent/path/to/file.txt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CDN Class plugin",
|
||||||
|
cfg: &config.PluginManagementCfg{
|
||||||
|
PluginsCDNURLTemplate: "https://cdn.example.com",
|
||||||
|
},
|
||||||
|
pluginInfo: func() PluginInfo {
|
||||||
|
return NewPluginInfo(plugins.JSONData{ID: "parent", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassCDN, pluginFS("https://cdn.example.com/plugins/parent"), nil)
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
module: "https://cdn.example.com/plugins/parent/module.js",
|
||||||
|
baseURL: "https://cdn.example.com/plugins/parent",
|
||||||
|
relURL: "https://cdn.example.com/plugins/parent/path/to/file.txt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CDN Class plugin with child",
|
||||||
|
cfg: &config.PluginManagementCfg{
|
||||||
|
PluginsCDNURLTemplate: "https://cdn.example.com",
|
||||||
|
},
|
||||||
|
pluginInfo: func() PluginInfo {
|
||||||
|
// Note: fake plugin FS is the most convenient way to mock the plugin FS for CDN plugins
|
||||||
|
parentInfo := NewPluginInfo(plugins.JSONData{ID: "parent", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassCDN, pluginFS("https://cdn.example.com/parent"), nil)
|
||||||
|
childInfo := NewPluginInfo(plugins.JSONData{ID: "child", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassCDN, pluginFS("https://cdn.example.com/parent/some/other/dir/child"), &parentInfo)
|
||||||
|
return childInfo
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
module: "https://cdn.example.com/parent/some/other/dir/child/module.js",
|
||||||
|
baseURL: "https://cdn.example.com/parent/some/other/dir/child",
|
||||||
|
relURL: "https://cdn.example.com/parent/some/other/dir/child/path/to/file.txt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CDN supported plugin",
|
||||||
|
cfg: &config.PluginManagementCfg{
|
||||||
|
PluginsCDNURLTemplate: "https://cdn.example.com",
|
||||||
|
PluginSettings: map[string]map[string]string{
|
||||||
|
"parent": {"cdn": "true"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pluginInfo: func() PluginInfo {
|
||||||
|
return NewPluginInfo(plugins.JSONData{ID: "parent", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassExternal, plugins.NewLocalFS("/plugins/parent"), nil)
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
module: "https://cdn.example.com/parent/1.0.0/public/plugins/parent/module.js",
|
||||||
|
baseURL: "https://cdn.example.com/parent/1.0.0/public/plugins/parent",
|
||||||
|
relURL: "https://cdn.example.com/parent/1.0.0/public/plugins/parent/path/to/file.txt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CDN supported plugin with child",
|
||||||
|
cfg: &config.PluginManagementCfg{
|
||||||
|
PluginsCDNURLTemplate: "https://cdn.example.com",
|
||||||
|
PluginSettings: map[string]map[string]string{
|
||||||
|
"parent": {"cdn": "true"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pluginInfo: func() PluginInfo {
|
||||||
|
parentInfo := NewPluginInfo(plugins.JSONData{ID: "parent", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassExternal, plugins.NewLocalFS("/plugins/parent"), nil)
|
||||||
|
childInfo := NewPluginInfo(plugins.JSONData{ID: "child", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassExternal, plugins.NewLocalFS("/plugins/parent/child"), &parentInfo)
|
||||||
|
return childInfo
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
module: "https://cdn.example.com/parent/1.0.0/public/plugins/parent/child/module.js",
|
||||||
|
baseURL: "https://cdn.example.com/parent/1.0.0/public/plugins/parent/child",
|
||||||
|
relURL: "https://cdn.example.com/parent/1.0.0/public/plugins/parent/child/path/to/file.txt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
svc := ProvideService(tc.cfg, pluginscdn.ProvideService(tc.cfg))
|
||||||
|
|
||||||
|
module, err := svc.Module(tc.pluginInfo())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expected.module, module)
|
||||||
|
|
||||||
|
baseURL, err := svc.Base(tc.pluginInfo())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expected.baseURL, baseURL)
|
||||||
|
|
||||||
|
relURL, err := svc.RelativeURL(tc.pluginInfo(), "path/to/file.txt")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expected.relURL, relURL)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -176,6 +176,7 @@ type PluginMetaDTO struct {
|
|||||||
BaseURL string `json:"baseUrl"`
|
BaseURL string `json:"baseUrl"`
|
||||||
Angular AngularMeta `json:"angular"`
|
Angular AngularMeta `json:"angular"`
|
||||||
MultiValueFilterOperators bool `json:"multiValueFilterOperators"`
|
MultiValueFilterOperators bool `json:"multiValueFilterOperators"`
|
||||||
|
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataSourceDTO struct {
|
type DataSourceDTO struct {
|
||||||
@ -211,28 +212,28 @@ type DataSourceDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PanelDTO struct {
|
type PanelDTO struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
AliasIDs []string `json:"aliasIds,omitempty"`
|
AliasIDs []string `json:"aliasIds,omitempty"`
|
||||||
Info Info `json:"info"`
|
Info Info `json:"info"`
|
||||||
HideFromList bool `json:"hideFromList"`
|
HideFromList bool `json:"hideFromList"`
|
||||||
Sort int `json:"sort"`
|
Sort int `json:"sort"`
|
||||||
SkipDataQuery bool `json:"skipDataQuery"`
|
SkipDataQuery bool `json:"skipDataQuery"`
|
||||||
ReleaseState string `json:"state"`
|
ReleaseState string `json:"state"`
|
||||||
BaseURL string `json:"baseUrl"`
|
BaseURL string `json:"baseUrl"`
|
||||||
Signature string `json:"signature"`
|
Signature string `json:"signature"`
|
||||||
Module string `json:"module"`
|
Module string `json:"module"`
|
||||||
|
Angular AngularMeta `json:"angular"`
|
||||||
Angular AngularMeta `json:"angular"`
|
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppDTO struct {
|
type AppDTO struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Preload bool `json:"preload"`
|
Preload bool `json:"preload"`
|
||||||
|
Angular AngularMeta `json:"angular"`
|
||||||
Angular AngularMeta `json:"angular"`
|
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -252,6 +253,13 @@ type Error struct {
|
|||||||
message string `json:"-"`
|
message string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LoadingStrategy string
|
||||||
|
|
||||||
|
const (
|
||||||
|
LoadingStrategyFetch LoadingStrategy = "fetch"
|
||||||
|
LoadingStrategyScript LoadingStrategy = "script"
|
||||||
|
)
|
||||||
|
|
||||||
func (e Error) Error() string {
|
func (e Error) Error() string {
|
||||||
if e.message != "" {
|
if e.message != "" {
|
||||||
return e.message
|
return e.message
|
||||||
|
@ -1243,8 +1243,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
|||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Module: "public/plugins/test-panel/module.js",
|
Module: "public/plugins/test-datasource/nested/module.js",
|
||||||
BaseURL: "public/plugins/test-panel",
|
BaseURL: "public/plugins/test-datasource/nested",
|
||||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent/nested")),
|
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent/nested")),
|
||||||
Signature: plugins.SignatureStatusValid,
|
Signature: plugins.SignatureStatusValid,
|
||||||
SignatureType: plugins.SignatureTypeGrafana,
|
SignatureType: plugins.SignatureTypeGrafana,
|
||||||
@ -1408,8 +1408,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
|||||||
{Name: "License", URL: "https://github.com/grafana/grafana-starter-panel/blob/master/LICENSE"},
|
{Name: "License", URL: "https://github.com/grafana/grafana-starter-panel/blob/master/LICENSE"},
|
||||||
},
|
},
|
||||||
Logos: plugins.Logos{
|
Logos: plugins.Logos{
|
||||||
Small: "public/plugins/myorgid-simple-panel/img/logo.svg",
|
Small: "public/plugins/myorgid-simple-app/child/img/logo.svg",
|
||||||
Large: "public/plugins/myorgid-simple-panel/img/logo.svg",
|
Large: "public/plugins/myorgid-simple-app/child/img/logo.svg",
|
||||||
},
|
},
|
||||||
Screenshots: []plugins.Screenshots{},
|
Screenshots: []plugins.Screenshots{},
|
||||||
Description: "Grafana Panel Plugin Template",
|
Description: "Grafana Panel Plugin Template",
|
||||||
@ -1423,8 +1423,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
|||||||
Plugins: []plugins.Dependency{},
|
Plugins: []plugins.Dependency{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Module: "public/plugins/myorgid-simple-panel/module.js",
|
Module: "public/plugins/myorgid-simple-app/child/module.js",
|
||||||
BaseURL: "public/plugins/myorgid-simple-panel",
|
BaseURL: "public/plugins/myorgid-simple-app/child",
|
||||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist/child")),
|
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist/child")),
|
||||||
IncludedInAppID: parent.ID,
|
IncludedInAppID: parent.ID,
|
||||||
Signature: plugins.SignatureStatusValid,
|
Signature: plugins.SignatureStatusValid,
|
||||||
|
81
pkg/services/pluginsintegration/pluginassets/pluginassets.go
Normal file
81
pkg/services/pluginsintegration/pluginassets/pluginassets.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package pluginassets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/Masterminds/semver/v3"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CreatePluginVersionCfgKey = "create_plugin_version"
|
||||||
|
CreatePluginVersionScriptSupportEnabled = "4.15.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
scriptLoadingMinSupportedVersion = semver.MustParse(CreatePluginVersionScriptSupportEnabled)
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProvideService(cfg *setting.Cfg, cdn *pluginscdn.Service) *Service {
|
||||||
|
return &Service{
|
||||||
|
cfg: cfg,
|
||||||
|
cdn: cdn,
|
||||||
|
log: log.New("pluginassets"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
cfg *setting.Cfg
|
||||||
|
cdn *pluginscdn.Service
|
||||||
|
log log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadingStrategy calculates the loading strategy for a plugin.
|
||||||
|
// If a plugin has plugin setting `create_plugin_version` >= 4.15.0, set loadingStrategy to "script".
|
||||||
|
// If a plugin is not loaded via the CDN and is not Angular, set loadingStrategy to "script".
|
||||||
|
// Otherwise, set loadingStrategy to "fetch".
|
||||||
|
func (s *Service) LoadingStrategy(_ context.Context, p pluginstore.Plugin) plugins.LoadingStrategy {
|
||||||
|
if pCfg, ok := s.cfg.PluginSettings[p.ID]; ok {
|
||||||
|
if s.compatibleCreatePluginVersion(pCfg) {
|
||||||
|
return plugins.LoadingStrategyScript
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the plugin has a parent, check the parent's create_plugin_version setting
|
||||||
|
if p.Parent != nil {
|
||||||
|
if pCfg, ok := s.cfg.PluginSettings[p.Parent.ID]; ok {
|
||||||
|
if s.compatibleCreatePluginVersion(pCfg) {
|
||||||
|
return plugins.LoadingStrategyScript
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.cndEnabled(p) && !p.Angular.Detected {
|
||||||
|
return plugins.LoadingStrategyScript
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins.LoadingStrategyFetch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool {
|
||||||
|
if cpv, ok := ps[CreatePluginVersionCfgKey]; ok {
|
||||||
|
createPluginVer, err := semver.NewVersion(cpv)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("Failed to parse create plugin version setting as semver", "version", cpv, "error", err)
|
||||||
|
} else {
|
||||||
|
if !createPluginVer.LessThan(scriptLoadingMinSupportedVersion) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) cndEnabled(p pluginstore.Plugin) bool {
|
||||||
|
return s.cdn.PluginSupported(p.ID) || p.Class == plugins.ClassCDN
|
||||||
|
}
|
@ -0,0 +1,168 @@
|
|||||||
|
package pluginassets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/config"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_Calculate(t *testing.T) {
|
||||||
|
const pluginID = "grafana-test-datasource"
|
||||||
|
|
||||||
|
const (
|
||||||
|
incompatVersion = "4.14.0"
|
||||||
|
compatVersion = CreatePluginVersionScriptSupportEnabled
|
||||||
|
futureVersion = "5.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
tcs := []struct {
|
||||||
|
name string
|
||||||
|
pluginSettings setting.PluginSettings
|
||||||
|
plugin pluginstore.Plugin
|
||||||
|
expected plugins.LoadingStrategy
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Expected LoadingStrategyScript when create-plugin version is compatible and plugin is not angular",
|
||||||
|
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||||
|
CreatePluginVersionCfgKey: compatVersion,
|
||||||
|
}),
|
||||||
|
plugin: newPlugin(pluginID, false),
|
||||||
|
expected: plugins.LoadingStrategyScript,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Expected LoadingStrategyScript when parent create-plugin version is compatible and plugin is not angular",
|
||||||
|
pluginSettings: newPluginSettings("parent-datasource", map[string]string{
|
||||||
|
CreatePluginVersionCfgKey: compatVersion,
|
||||||
|
}),
|
||||||
|
plugin: newPlugin(pluginID, false, func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
|
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
|
||||||
|
return p
|
||||||
|
}),
|
||||||
|
expected: plugins.LoadingStrategyScript,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Expected LoadingStrategyScript when create-plugin version is future compatible and plugin is not angular",
|
||||||
|
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||||
|
CreatePluginVersionCfgKey: futureVersion,
|
||||||
|
}),
|
||||||
|
plugin: newPlugin(pluginID, false),
|
||||||
|
expected: plugins.LoadingStrategyScript,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Expected LoadingStrategyScript when create-plugin version is not provided, plugin is not angular and is not configured as CDN enabled",
|
||||||
|
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||||
|
// NOTE: cdn key is not set
|
||||||
|
}),
|
||||||
|
plugin: newPlugin(pluginID, false),
|
||||||
|
expected: plugins.LoadingStrategyScript,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Expected LoadingStrategyScript when create-plugin version is not compatible, plugin is not angular, is not configured as CDN enabled and does not have the CDN class",
|
||||||
|
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||||
|
CreatePluginVersionCfgKey: incompatVersion,
|
||||||
|
// NOTE: cdn key is not set
|
||||||
|
}),
|
||||||
|
plugin: newPlugin(pluginID, false, func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
|
p.Class = plugins.ClassExternal
|
||||||
|
return p
|
||||||
|
}),
|
||||||
|
expected: plugins.LoadingStrategyScript,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Expected LoadingStrategyFetch when create-plugin version is not compatible, plugin is not angular, is configured as CDN enabled and does not have the CDN class",
|
||||||
|
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||||
|
"cdn": "true",
|
||||||
|
CreatePluginVersionCfgKey: incompatVersion,
|
||||||
|
}),
|
||||||
|
plugin: newPlugin(pluginID, false, func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
|
p.Class = plugins.ClassExternal
|
||||||
|
return p
|
||||||
|
}),
|
||||||
|
expected: plugins.LoadingStrategyFetch,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Expected LoadingStrategyFetch when create-plugin version is not compatible and plugin is angular",
|
||||||
|
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||||
|
CreatePluginVersionCfgKey: incompatVersion,
|
||||||
|
}),
|
||||||
|
plugin: newPlugin(pluginID, true),
|
||||||
|
expected: plugins.LoadingStrategyFetch,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Expected LoadingStrategyFetch when create-plugin version is not compatible, plugin is not angular and plugin is configured as CDN enabled",
|
||||||
|
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||||
|
"cdn": "true",
|
||||||
|
CreatePluginVersionCfgKey: incompatVersion,
|
||||||
|
}),
|
||||||
|
plugin: newPlugin(pluginID, false),
|
||||||
|
expected: plugins.LoadingStrategyFetch,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Expected LoadingStrategyFetch when create-plugin version is not compatible, plugin is not angular and has the CDN class",
|
||||||
|
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||||
|
CreatePluginVersionCfgKey: incompatVersion,
|
||||||
|
}),
|
||||||
|
plugin: newPlugin(pluginID, false, func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||||
|
p.Class = plugins.ClassCDN
|
||||||
|
return p
|
||||||
|
}),
|
||||||
|
expected: plugins.LoadingStrategyFetch,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Expected LoadingStrategyScript when plugin setting create-plugin version is badly formatted, plugin is not configured as CDN enabled and does not have the CDN class",
|
||||||
|
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||||
|
CreatePluginVersionCfgKey: "invalidSemver",
|
||||||
|
}),
|
||||||
|
plugin: newPlugin(pluginID, false),
|
||||||
|
expected: plugins.LoadingStrategyScript,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
s := &Service{
|
||||||
|
cfg: newCfg(tc.pluginSettings),
|
||||||
|
cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{
|
||||||
|
PluginsCDNURLTemplate: "http://cdn.example.com", // required for cdn.PluginSupported check
|
||||||
|
PluginSettings: tc.pluginSettings,
|
||||||
|
}),
|
||||||
|
log: log.NewNopLogger(),
|
||||||
|
}
|
||||||
|
|
||||||
|
got := s.LoadingStrategy(context.Background(), tc.plugin)
|
||||||
|
assert.Equal(t, tc.expected, got, "unexpected loading strategy")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPlugin(pluginID string, angular bool, cbs ...func(p pluginstore.Plugin) pluginstore.Plugin) pluginstore.Plugin {
|
||||||
|
p := pluginstore.Plugin{
|
||||||
|
JSONData: plugins.JSONData{
|
||||||
|
ID: pluginID,
|
||||||
|
},
|
||||||
|
Angular: plugins.AngularMeta{Detected: angular},
|
||||||
|
}
|
||||||
|
for _, cb := range cbs {
|
||||||
|
p = cb(p)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCfg(ps setting.PluginSettings) *setting.Cfg {
|
||||||
|
return &setting.Cfg{
|
||||||
|
PluginSettings: ps,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPluginSettings(pluginID string, kv map[string]string) setting.PluginSettings {
|
||||||
|
return setting.PluginSettings{
|
||||||
|
pluginID: kv,
|
||||||
|
}
|
||||||
|
}
|
@ -43,6 +43,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/loader"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/loader"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
|
||||||
@ -126,6 +127,7 @@ var WireSet = wire.NewSet(
|
|||||||
plugincontext.ProvideBaseService,
|
plugincontext.ProvideBaseService,
|
||||||
wire.Bind(new(plugincontext.BasePluginContextProvider), new(*plugincontext.BaseProvider)),
|
wire.Bind(new(plugincontext.BasePluginContextProvider), new(*plugincontext.BaseProvider)),
|
||||||
plugininstaller.ProvideService,
|
plugininstaller.ProvideService,
|
||||||
|
pluginassets.ProvideService,
|
||||||
)
|
)
|
||||||
|
|
||||||
// WireExtensionSet provides a wire.ProviderSet of plugin providers that can be
|
// WireExtensionSet provides a wire.ProviderSet of plugin providers that can be
|
||||||
|
@ -16,6 +16,7 @@ type Plugin struct {
|
|||||||
Class plugins.Class
|
Class plugins.Class
|
||||||
|
|
||||||
// App fields
|
// App fields
|
||||||
|
Parent *ParentPlugin
|
||||||
IncludedInAppID string
|
IncludedInAppID string
|
||||||
DefaultNavURL string
|
DefaultNavURL string
|
||||||
Pinned bool
|
Pinned bool
|
||||||
@ -59,7 +60,7 @@ func ToGrafanaDTO(p *plugins.Plugin) Plugin {
|
|||||||
supportsStreaming = true
|
supportsStreaming = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return Plugin{
|
dto := Plugin{
|
||||||
fs: p.FS,
|
fs: p.FS,
|
||||||
supportsStreaming: supportsStreaming,
|
supportsStreaming: supportsStreaming,
|
||||||
Class: p.Class,
|
Class: p.Class,
|
||||||
@ -76,4 +77,14 @@ func ToGrafanaDTO(p *plugins.Plugin) Plugin {
|
|||||||
ExternalService: p.ExternalService,
|
ExternalService: p.ExternalService,
|
||||||
Angular: p.Angular,
|
Angular: p.Angular,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.Parent != nil {
|
||||||
|
dto.Parent = &ParentPlugin{ID: p.Parent.ID}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParentPlugin struct {
|
||||||
|
ID string
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { http, HttpResponse } from 'msw';
|
import { http, HttpResponse } from 'msw';
|
||||||
|
|
||||||
import { PluginMeta } from '@grafana/data';
|
import { PluginLoadingStrategy, PluginMeta } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { plugins } from 'app/features/alerting/unified/testSetup/plugins';
|
import { plugins } from 'app/features/alerting/unified/testSetup/plugins';
|
||||||
|
|
||||||
@ -18,6 +18,7 @@ export const getPluginsHandler = (pluginsArray: PluginMeta[] = plugins) => {
|
|||||||
preload: true,
|
preload: true,
|
||||||
version: info.version,
|
version: info.version,
|
||||||
angular: angular ?? { detected: false, hideDeprecation: false },
|
angular: angular ?? { detected: false, hideDeprecation: false },
|
||||||
|
loadingStrategy: PluginLoadingStrategy.script,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { PluginLoadingStrategy } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { RuleGroupIdentifier } from 'app/types/unified-alerting';
|
import { RuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
@ -47,6 +48,7 @@ describe('getRuleOrigin', () => {
|
|||||||
path: '',
|
path: '',
|
||||||
preload: true,
|
preload: true,
|
||||||
angular: { detected: false, hideDeprecation: false },
|
angular: { detected: false, hideDeprecation: false },
|
||||||
|
loadingStrategy: PluginLoadingStrategy.script,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const rule = mockCombinedRule({
|
const rule = mockCombinedRule({
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ComponentType } from 'react';
|
import { ComponentType } from 'react';
|
||||||
|
|
||||||
import { PanelPlugin, PanelPluginMeta, PanelProps } from '@grafana/data';
|
import { PanelPlugin, PanelPluginMeta, PanelProps, PluginLoadingStrategy } from '@grafana/data';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
|
||||||
import { getPanelPluginLoadError } from '../panel/components/PanelPluginError';
|
import { getPanelPluginLoadError } from '../panel/components/PanelPluginError';
|
||||||
@ -56,10 +56,12 @@ export function syncGetPanelPlugin(id: string): PanelPlugin | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPanelPlugin(meta: PanelPluginMeta): Promise<PanelPlugin> {
|
function getPanelPlugin(meta: PanelPluginMeta): Promise<PanelPlugin> {
|
||||||
|
const fallbackLoadingStrategy = meta.loadingStrategy ?? PluginLoadingStrategy.fetch;
|
||||||
return importPluginModule({
|
return importPluginModule({
|
||||||
path: meta.module,
|
path: meta.module,
|
||||||
version: meta.info?.version,
|
version: meta.info?.version,
|
||||||
isAngular: meta.angular?.detected,
|
isAngular: meta.angular?.detected,
|
||||||
|
loadingStrategy: fallbackLoadingStrategy,
|
||||||
pluginId: meta.id,
|
pluginId: meta.id,
|
||||||
})
|
})
|
||||||
.then((pluginExports) => {
|
.then((pluginExports) => {
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { registerPluginInCache, invalidatePluginInCache, resolveWithCache, getPluginFromCache } from './cache';
|
import { PluginLoadingStrategy } from '@grafana/data';
|
||||||
|
|
||||||
|
import {
|
||||||
|
registerPluginInCache,
|
||||||
|
invalidatePluginInCache,
|
||||||
|
resolveWithCache,
|
||||||
|
getPluginFromCache,
|
||||||
|
extractCacheKeyFromPath,
|
||||||
|
} from './cache';
|
||||||
|
|
||||||
jest.mock('./constants', () => ({
|
jest.mock('./constants', () => ({
|
||||||
CACHE_INITIALISED_AT: 123456,
|
CACHE_INITIALISED_AT: 123456,
|
||||||
@ -7,28 +15,28 @@ jest.mock('./constants', () => ({
|
|||||||
describe('Cache Functions', () => {
|
describe('Cache Functions', () => {
|
||||||
describe('registerPluginInCache', () => {
|
describe('registerPluginInCache', () => {
|
||||||
it('should register a plugin in the cache', () => {
|
it('should register a plugin in the cache', () => {
|
||||||
const plugin = { pluginId: 'plugin1', version: '1.0.0', isAngular: false };
|
const plugin = { version: '1.0.0', loadingStrategy: PluginLoadingStrategy.script };
|
||||||
registerPluginInCache(plugin);
|
registerPluginInCache({ path: 'public/plugins/plugin1/module.js', ...plugin });
|
||||||
expect(getPluginFromCache('plugin1')).toEqual(plugin);
|
expect(getPluginFromCache('plugin1')).toEqual(plugin);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not register a plugin if it already exists in the cache', () => {
|
it('should not register a plugin if it already exists in the cache', () => {
|
||||||
const pluginId = 'plugin2';
|
const path = 'public/plugins/plugin2/module.js';
|
||||||
const plugin = { pluginId, version: '2.0.0' };
|
const plugin = { path, version: '2.0.0', loadingStrategy: PluginLoadingStrategy.script };
|
||||||
registerPluginInCache(plugin);
|
registerPluginInCache(plugin);
|
||||||
const plugin2 = { pluginId, version: '2.5.0' };
|
const plugin2 = { path, version: '2.5.0', loadingStrategy: PluginLoadingStrategy.script };
|
||||||
registerPluginInCache(plugin2);
|
registerPluginInCache(plugin2);
|
||||||
expect(getPluginFromCache(pluginId)?.version).toBe('2.0.0');
|
expect(getPluginFromCache(path)?.version).toBe('2.0.0');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('invalidatePluginInCache', () => {
|
describe('invalidatePluginInCache', () => {
|
||||||
it('should invalidate a plugin in the cache', () => {
|
it('should invalidate a plugin in the cache', () => {
|
||||||
const pluginId = 'plugin3';
|
const path = 'public/plugins/plugin2/module.js';
|
||||||
const plugin = { pluginId, version: '3.0.0' };
|
const plugin = { path, version: '3.0.0', loadingStrategy: PluginLoadingStrategy.script };
|
||||||
registerPluginInCache(plugin);
|
registerPluginInCache(plugin);
|
||||||
invalidatePluginInCache(pluginId);
|
invalidatePluginInCache('plugin2');
|
||||||
expect(getPluginFromCache(pluginId)).toBeUndefined();
|
expect(getPluginFromCache('plugin2')).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not throw an error if the plugin does not exist in the cache', () => {
|
it('should not throw an error if the plugin does not exist in the cache', () => {
|
||||||
@ -43,17 +51,43 @@ describe('Cache Functions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve URL with plugin version as cache bust parameter if available', () => {
|
it('should resolve URL with plugin version as cache bust parameter if available', () => {
|
||||||
const plugin = { pluginId: 'plugin5', version: '5.0.0' };
|
|
||||||
registerPluginInCache(plugin);
|
|
||||||
const url = 'http://localhost:3000/public/plugins/plugin5/module.js';
|
const url = 'http://localhost:3000/public/plugins/plugin5/module.js';
|
||||||
|
const plugin = { path: url, version: '5.0.0', loadingStrategy: PluginLoadingStrategy.script };
|
||||||
|
registerPluginInCache(plugin);
|
||||||
expect(resolveWithCache(url)).toContain('_cache=5.0.0');
|
expect(resolveWithCache(url)).toContain('_cache=5.0.0');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('extractCacheKeyFromPath', () => {
|
||||||
|
it('should extract plugin ID from a path', () => {
|
||||||
|
expect(extractCacheKeyFromPath('public/plugins/plugin6/module.js')).toBe('plugin6');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract plugin ID from a path', () => {
|
||||||
|
expect(extractCacheKeyFromPath('public/plugins/plugin6/datasource/module.js')).toBe('plugin6');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract plugin ID from a url', () => {
|
||||||
|
expect(extractCacheKeyFromPath('https://my-url.com/plugin6/1.0.0/public/plugins/plugin6/module.js')).toBe(
|
||||||
|
'plugin6'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract plugin ID from a nested plugin url', () => {
|
||||||
|
expect(
|
||||||
|
extractCacheKeyFromPath('https://my-url.com/plugin6/1.0.0/public/plugins/plugin6/datasource/module.js')
|
||||||
|
).toBe('plugin6');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null if the path does not match the pattern', () => {
|
||||||
|
expect(extractCacheKeyFromPath('public/plugins/plugin7')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getPluginFromCache', () => {
|
describe('getPluginFromCache', () => {
|
||||||
it('should return plugin from cache if exists', () => {
|
it('should return plugin from cache if exists', () => {
|
||||||
const plugin = { pluginId: 'plugin6', version: '6.0.0' };
|
const plugin = { version: '6.0.0', loadingStrategy: PluginLoadingStrategy.script };
|
||||||
registerPluginInCache(plugin);
|
registerPluginInCache({ path: 'public/plugins/plugin6/module.js', ...plugin });
|
||||||
expect(getPluginFromCache('plugin6')).toEqual(plugin);
|
expect(getPluginFromCache('plugin6')).toEqual(plugin);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,22 +1,26 @@
|
|||||||
|
import { PluginLoadingStrategy } from '@grafana/data';
|
||||||
|
|
||||||
import { clearPluginSettingsCache } from '../pluginSettings';
|
import { clearPluginSettingsCache } from '../pluginSettings';
|
||||||
|
|
||||||
import { CACHE_INITIALISED_AT } from './constants';
|
import { CACHE_INITIALISED_AT } from './constants';
|
||||||
|
|
||||||
const cache: Record<string, CacheablePlugin> = {};
|
const cache: Record<string, CachedPlugin> = {};
|
||||||
|
|
||||||
type CacheablePlugin = {
|
type CacheablePlugin = {
|
||||||
pluginId: string;
|
path: string;
|
||||||
version: string;
|
version: string;
|
||||||
isAngular?: boolean;
|
loadingStrategy: PluginLoadingStrategy;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function registerPluginInCache({ pluginId, version, isAngular }: CacheablePlugin): void {
|
type CachedPlugin = Omit<CacheablePlugin, 'path'>;
|
||||||
const key = pluginId;
|
|
||||||
|
export function registerPluginInCache({ path, version, loadingStrategy }: CacheablePlugin): void {
|
||||||
|
const key = extractCacheKeyFromPath(path);
|
||||||
|
|
||||||
if (key && !cache[key]) {
|
if (key && !cache[key]) {
|
||||||
cache[key] = {
|
cache[key] = {
|
||||||
version: encodeURI(version),
|
version: encodeURI(version),
|
||||||
isAngular,
|
loadingStrategy,
|
||||||
pluginId,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,7 +43,7 @@ export function resolveWithCache(url: string, defaultBust = CACHE_INITIALISED_AT
|
|||||||
return `${url}?_cache=${bust}`;
|
return `${url}?_cache=${bust}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPluginFromCache(path: string): CacheablePlugin | undefined {
|
export function getPluginFromCache(path: string): CachedPlugin | undefined {
|
||||||
const key = getCacheKey(path);
|
const key = getCacheKey(path);
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return;
|
return;
|
||||||
@ -47,6 +51,12 @@ export function getPluginFromCache(path: string): CacheablePlugin | undefined {
|
|||||||
return cache[key];
|
return cache[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractCacheKeyFromPath(path: string) {
|
||||||
|
const regex = /\/?public\/plugins\/([^\/]+)\//;
|
||||||
|
const match = path.match(regex);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
function getCacheKey(address: string): string | undefined {
|
function getCacheKey(address: string): string | undefined {
|
||||||
const key = Object.keys(cache).find((key) => address.includes(key));
|
const key = Object.keys(cache).find((key) => address.includes(key));
|
||||||
if (!key) {
|
if (!key) {
|
||||||
|
@ -5,7 +5,7 @@ import { startMeasure, stopMeasure } from 'app/core/utils/metrics';
|
|||||||
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
||||||
|
|
||||||
import { PluginExtensionRegistries } from './extensions/registry/types';
|
import { PluginExtensionRegistries } from './extensions/registry/types';
|
||||||
import * as pluginLoader from './plugin_loader';
|
import { importPluginModule } from './plugin_loader';
|
||||||
|
|
||||||
export type PluginPreloadResult = {
|
export type PluginPreloadResult = {
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
@ -48,14 +48,15 @@ export async function preloadPlugins(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function preload(config: AppPluginConfig): Promise<PluginPreloadResult> {
|
async function preload(config: AppPluginConfig): Promise<PluginPreloadResult> {
|
||||||
const { path, version, id: pluginId } = config;
|
const { path, version, id: pluginId, loadingStrategy } = config;
|
||||||
try {
|
try {
|
||||||
startMeasure(`frontend_plugin_preload_${pluginId}`);
|
startMeasure(`frontend_plugin_preload_${pluginId}`);
|
||||||
const { plugin } = await pluginLoader.importPluginModule({
|
const { plugin } = await importPluginModule({
|
||||||
path,
|
path,
|
||||||
version,
|
version,
|
||||||
isAngular: config.angular.detected,
|
isAngular: config.angular.detected,
|
||||||
pluginId,
|
pluginId,
|
||||||
|
loadingStrategy,
|
||||||
});
|
});
|
||||||
const { exposedComponentConfigs = [], addedComponentConfigs = [], addedLinkConfigs = [] } = plugin;
|
const { exposedComponentConfigs = [], addedComponentConfigs = [], addedLinkConfigs = [] } = plugin;
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
DataSourceJsonData,
|
DataSourceJsonData,
|
||||||
DataSourcePlugin,
|
DataSourcePlugin,
|
||||||
DataSourcePluginMeta,
|
DataSourcePluginMeta,
|
||||||
|
PluginLoadingStrategy,
|
||||||
PluginMeta,
|
PluginMeta,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { DataQuery } from '@grafana/schema';
|
import { DataQuery } from '@grafana/schema';
|
||||||
@ -18,7 +19,7 @@ import { SystemJS } from './loader/systemjs';
|
|||||||
import { sharedDependenciesMap } from './loader/sharedDependencies';
|
import { sharedDependenciesMap } from './loader/sharedDependencies';
|
||||||
import { decorateSystemJSFetch, decorateSystemJSResolve, decorateSystemJsOnload } from './loader/systemjsHooks';
|
import { decorateSystemJSFetch, decorateSystemJSResolve, decorateSystemJsOnload } from './loader/systemjsHooks';
|
||||||
import { SystemJSWithLoaderHooks } from './loader/types';
|
import { SystemJSWithLoaderHooks } from './loader/types';
|
||||||
import { buildImportMap, isHostedOnCDN, resolveModulePath } from './loader/utils';
|
import { buildImportMap, resolveModulePath } from './loader/utils';
|
||||||
import { importPluginModuleInSandbox } from './sandbox/sandbox_plugin_loader';
|
import { importPluginModuleInSandbox } from './sandbox/sandbox_plugin_loader';
|
||||||
import { isFrontendSandboxSupported } from './sandbox/utils';
|
import { isFrontendSandboxSupported } from './sandbox/utils';
|
||||||
|
|
||||||
@ -29,13 +30,17 @@ SystemJS.addImportMap({ imports });
|
|||||||
const systemJSPrototype: SystemJSWithLoaderHooks = SystemJS.constructor.prototype;
|
const systemJSPrototype: SystemJSWithLoaderHooks = SystemJS.constructor.prototype;
|
||||||
|
|
||||||
// This instructs SystemJS to load plugin assets using fetch and eval if it returns a truthy value, otherwise
|
// This instructs SystemJS to load plugin assets using fetch and eval if it returns a truthy value, otherwise
|
||||||
// it will load the plugin using a script tag. We only want to fetch and eval files that are
|
// it will load the plugin using a script tag. The logic that sets loadingStrategy comes from the backend.
|
||||||
// hosted on a CDN, are related to Angular plugins or are not js files.
|
// See: pkg/services/pluginsintegration/pluginassets/pluginassets.go
|
||||||
systemJSPrototype.shouldFetch = function (url) {
|
systemJSPrototype.shouldFetch = function (url) {
|
||||||
const pluginInfo = getPluginFromCache(url);
|
const pluginInfo = getPluginFromCache(url);
|
||||||
const jsTypeRegEx = /^[^#?]+\.(js)([?#].*)?$/;
|
const jsTypeRegEx = /^[^#?]+\.(js)([?#].*)?$/;
|
||||||
|
|
||||||
return isHostedOnCDN(url) || Boolean(pluginInfo?.isAngular) || !jsTypeRegEx.test(url);
|
if (!jsTypeRegEx.test(url)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean(pluginInfo?.loadingStrategy !== PluginLoadingStrategy.script);
|
||||||
};
|
};
|
||||||
|
|
||||||
const originalImport = systemJSPrototype.import;
|
const originalImport = systemJSPrototype.import;
|
||||||
@ -64,17 +69,19 @@ systemJSPrototype.onload = decorateSystemJsOnload;
|
|||||||
|
|
||||||
export async function importPluginModule({
|
export async function importPluginModule({
|
||||||
path,
|
path,
|
||||||
|
pluginId,
|
||||||
|
loadingStrategy,
|
||||||
version,
|
version,
|
||||||
isAngular,
|
isAngular,
|
||||||
pluginId,
|
|
||||||
}: {
|
}: {
|
||||||
path: string;
|
path: string;
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
|
loadingStrategy: PluginLoadingStrategy;
|
||||||
version?: string;
|
version?: string;
|
||||||
isAngular?: boolean;
|
isAngular?: boolean;
|
||||||
}): Promise<System.Module> {
|
}): Promise<System.Module> {
|
||||||
if (version) {
|
if (version) {
|
||||||
registerPluginInCache({ pluginId, version, isAngular });
|
registerPluginInCache({ path, version, loadingStrategy });
|
||||||
}
|
}
|
||||||
|
|
||||||
const builtIn = builtInPlugins[path];
|
const builtIn = builtInPlugins[path];
|
||||||
@ -99,10 +106,12 @@ export async function importPluginModule({
|
|||||||
|
|
||||||
export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise<GenericDataSourcePlugin> {
|
export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise<GenericDataSourcePlugin> {
|
||||||
const isAngular = meta.angular?.detected ?? meta.angularDetected;
|
const isAngular = meta.angular?.detected ?? meta.angularDetected;
|
||||||
|
const fallbackLoadingStrategy = meta.loadingStrategy ?? PluginLoadingStrategy.fetch;
|
||||||
return importPluginModule({
|
return importPluginModule({
|
||||||
path: meta.module,
|
path: meta.module,
|
||||||
version: meta.info?.version,
|
version: meta.info?.version,
|
||||||
isAngular,
|
isAngular,
|
||||||
|
loadingStrategy: fallbackLoadingStrategy,
|
||||||
pluginId: meta.id,
|
pluginId: meta.id,
|
||||||
}).then((pluginExports) => {
|
}).then((pluginExports) => {
|
||||||
if (pluginExports.plugin) {
|
if (pluginExports.plugin) {
|
||||||
@ -128,10 +137,12 @@ export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise<Gene
|
|||||||
|
|
||||||
export function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> {
|
export function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> {
|
||||||
const isAngular = meta.angular?.detected ?? meta.angularDetected;
|
const isAngular = meta.angular?.detected ?? meta.angularDetected;
|
||||||
|
const fallbackLoadingStrategy = meta.loadingStrategy ?? PluginLoadingStrategy.fetch;
|
||||||
return importPluginModule({
|
return importPluginModule({
|
||||||
path: meta.module,
|
path: meta.module,
|
||||||
version: meta.info?.version,
|
version: meta.info?.version,
|
||||||
isAngular,
|
isAngular,
|
||||||
|
loadingStrategy: fallbackLoadingStrategy,
|
||||||
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();
|
||||||
|
Reference in New Issue
Block a user