diff --git a/.betterer.results b/.betterer.results index 429de56bad5..f3f9ab40a6f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2645,9 +2645,6 @@ exports[`better eslint`] = { "public/app/features/plugins/admin/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/features/plugins/built_in_plugins.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/features/plugins/components/PluginsErrorsInfo.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] ], @@ -2678,17 +2675,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/features/plugins/plugin_loader.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Do not use any type assertions.", "7"], - [0, 0, 0, "Do not use any type assertions.", "8"] - ], "public/app/features/plugins/sandbox/distortion_map.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/jest.config.js b/jest.config.js index b17dcb00283..890dd12d4bd 100644 --- a/jest.config.js +++ b/jest.config.js @@ -41,6 +41,8 @@ module.exports = { // near-membrane-dom won't work in a nodejs environment. '@locker/near-membrane-dom': '/public/test/mocks/nearMembraneDom.ts', '^@grafana/schema/dist/esm/(.*)$': '/packages/grafana-schema/src/$1', + // prevent systemjs amd extra from breaking tests. + 'systemjs/dist/extras/amd': '/public/test/mocks/systemjsAMDExtra.ts', }, // Log the test results with dynamic Loki tags. Drone CI only reporters: ['default', ['/public/test/log-reporter.js', { enable: process.env.DRONE === 'true' }]], diff --git a/package.json b/package.json index 31d54ef1a77..6d8fbd435e3 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,7 @@ "@types/slate": "0.47.11", "@types/slate-plain-serializer": "0.7.2", "@types/slate-react": "0.22.9", + "@types/systemjs": "6.13.1", "@types/testing-library__jest-dom": "5.14.8", "@types/tinycolor2": "1.4.3", "@types/uuid": "9.0.2", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 29ccc0d0b14..9b9acadfcec 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -44,7 +44,8 @@ "history": "4.10.1", "lodash": "4.17.21", "rxjs": "7.8.1", - "systemjs": "0.20.19", + "systemjs": "6.14.2", + "systemjs-cjs-extra": "0.1.0", "tslib": "2.6.0" }, "devDependencies": { @@ -60,7 +61,7 @@ "@types/lodash": "4.14.195", "@types/react": "18.2.15", "@types/react-dom": "18.2.7", - "@types/systemjs": "^0.20.6", + "@types/systemjs": "6.13.1", "esbuild": "0.18.12", "lodash": "4.17.21", "react": "18.2.0", diff --git a/packages/grafana-runtime/src/utils/plugin.ts b/packages/grafana-runtime/src/utils/plugin.ts index a1024229a9c..8f11c6f811e 100644 --- a/packages/grafana-runtime/src/utils/plugin.ts +++ b/packages/grafana-runtime/src/utils/plugin.ts @@ -1,12 +1,13 @@ -// @ts-ignore -import System from 'systemjs/dist/system.js'; +import 'systemjs/dist/system'; +// Add ability to load plugins bundled as AMD format +import 'systemjs/dist/extras/amd'; +// Add ability to load plugins bundled as CJS format +import 'systemjs-cjs-extra'; import { PanelPlugin } from '@grafana/data'; import { config } from '../config'; -// @ts-ignore - /** * Option to specify a plugin css that should be applied for the dark * and the light theme. @@ -21,7 +22,7 @@ export interface PluginCssOptions { /** * @internal */ -export const SystemJS = System; +export const SystemJS = window.System; /** * Use this to load css for a Grafana plugin by specifying a {@link PluginCssOptions} @@ -30,9 +31,13 @@ export const SystemJS = System; * @param options - plugin styling for light and dark theme. * @public */ -export function loadPluginCss(options: PluginCssOptions): Promise { - const theme = config.bootData.user.lightTheme ? options.light : options.dark; - return SystemJS.import(`${theme}!css`); +export async function loadPluginCss(options: PluginCssOptions): Promise { + try { + const cssPath = config.bootData.user.theme === 'light' ? options.light : options.dark; + return await SystemJS.import(cssPath); + } catch (err) { + console.error(err); + } } interface PluginImportUtils { @@ -57,3 +62,11 @@ export function getPluginImportUtils(): PluginImportUtils { return pluginImportUtils; } + +// Grafana relies on RequireJS for Monaco Editor to load. +// The SystemJS AMD extra creates a global define which causes RequireJS to silently bail. +// Here we move and reset global define so Monaco Editor loader script continues to work. +// @ts-ignore +window.__grafana_amd_define = window.define; +// @ts-ignore +window.define = undefined; diff --git a/pkg/plugins/config/config.go b/pkg/plugins/config/config.go index 317e8bfc464..3c849754084 100644 --- a/pkg/plugins/config/config.go +++ b/pkg/plugins/config/config.go @@ -39,7 +39,8 @@ type Cfg struct { GrafanaComURL string - GrafanaAppURL string + GrafanaAppURL string + GrafanaAppSubURL string Features plugins.FeatureToggles @@ -48,7 +49,7 @@ type Cfg struct { func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSettings, pluginsAllowUnsigned []string, awsAllowedAuthProviders []string, awsAssumeRoleEnabled bool, awsExternalId string, azure *azsettings.AzureSettings, secureSocksDSProxy setting.SecureSocksDSProxySettings, - grafanaVersion string, logDatasourceRequests bool, pluginsCDNURLTemplate string, appURL string, tracing Tracing, features plugins.FeatureToggles, angularSupportEnabled bool, + grafanaVersion string, logDatasourceRequests bool, pluginsCDNURLTemplate string, appURL string, appSubURL string, tracing Tracing, features plugins.FeatureToggles, angularSupportEnabled bool, grafanaComURL string) *Cfg { return &Cfg{ log: log.New("plugin.cfg"), @@ -67,6 +68,7 @@ func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSetti Tracing: tracing, GrafanaComURL: grafanaComURL, GrafanaAppURL: appURL, + GrafanaAppSubURL: appSubURL, Features: features, AngularSupportEnabled: angularSupportEnabled, } diff --git a/pkg/plugins/manager/loader/assetpath/assetpath.go b/pkg/plugins/manager/loader/assetpath/assetpath.go index 0b093b0b753..bcdbc1a627d 100644 --- a/pkg/plugins/manager/loader/assetpath/assetpath.go +++ b/pkg/plugins/manager/loader/assetpath/assetpath.go @@ -17,47 +17,57 @@ import ( // on the plugins CDN, and it will switch to the correct implementation depending on the plugin and the config. type Service struct { cdn *pluginscdn.Service + cfg *config.Cfg } -func ProvideService(cdn *pluginscdn.Service) *Service { - return &Service{cdn: cdn} +func ProvideService(cfg *config.Cfg, cdn *pluginscdn.Service) *Service { + return &Service{cfg: cfg, cdn: cdn} +} + +type PluginInfo struct { + pluginJSON plugins.JSONData + class plugins.Class + dir string +} + +func NewPluginInfo(pluginJSON plugins.JSONData, class plugins.Class, fs plugins.FS) PluginInfo { + return PluginInfo{ + pluginJSON: pluginJSON, + class: class, + dir: fs.Base(), + } } func DefaultService(cfg *config.Cfg) *Service { - return &Service{cdn: pluginscdn.ProvideService(cfg)} + return &Service{cfg: cfg, cdn: pluginscdn.ProvideService(cfg)} } // Base returns the base path for the specified plugin. -func (s *Service) Base(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) (string, error) { - if class == plugins.ClassCore { - return path.Join("public/app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir)), nil +func (s *Service) Base(n PluginInfo) (string, error) { + if n.class == plugins.ClassCore { + return path.Join("/", s.cfg.GrafanaAppSubURL, "/public/app/plugins", string(n.pluginJSON.Type), filepath.Base(n.dir)), nil } - if s.cdn.PluginSupported(pluginJSON.ID) { - return s.cdn.SystemJSAssetPath(pluginJSON.ID, pluginJSON.Info.Version, "") + if s.cdn.PluginSupported(n.pluginJSON.ID) { + return s.cdn.AssetURL(n.pluginJSON.ID, n.pluginJSON.Info.Version, "") } - return path.Join("public/plugins", pluginJSON.ID), nil + return path.Join("/", s.cfg.GrafanaAppSubURL, "/public/plugins", n.pluginJSON.ID), nil } // Module returns the module.js path for the specified plugin. -func (s *Service) Module(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) (string, error) { - if class == plugins.ClassCore { - return path.Join("app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir), "module"), nil +func (s *Service) Module(n PluginInfo) (string, error) { + if n.class == plugins.ClassCore { + return path.Join("core:plugin", filepath.Base(n.dir)), nil } - if s.cdn.PluginSupported(pluginJSON.ID) { - return s.cdn.SystemJSAssetPath(pluginJSON.ID, pluginJSON.Info.Version, "module") + if s.cdn.PluginSupported(n.pluginJSON.ID) { + return s.cdn.AssetURL(n.pluginJSON.ID, n.pluginJSON.Info.Version, "module.js") } - return path.Join("plugins", pluginJSON.ID, "module"), nil + return path.Join("/", s.cfg.GrafanaAppSubURL, "/public/plugins", n.pluginJSON.ID, "module.js"), nil } // RelativeURL returns the relative URL for an arbitrary plugin asset. -// If pathStr is an empty string, defaultStr is returned. -func (s *Service) RelativeURL(p *plugins.Plugin, pathStr, defaultStr string) (string, error) { - if pathStr == "" { - return defaultStr, nil - } - if s.cdn.PluginSupported(p.ID) { - // CDN - return s.cdn.NewCDNURLConstructor(p.ID, p.Info.Version).StringPath(pathStr) +func (s *Service) RelativeURL(n PluginInfo, pathStr string) (string, error) { + if s.cdn.PluginSupported(n.pluginJSON.ID) { + return s.cdn.NewCDNURLConstructor(n.pluginJSON.ID, n.pluginJSON.Info.Version).StringPath(pathStr) } // Local u, err := url.Parse(pathStr) @@ -67,9 +77,20 @@ func (s *Service) RelativeURL(p *plugins.Plugin, pathStr, defaultStr string) (st if u.IsAbs() { return pathStr, nil } - // is set as default or has already been prefixed with base path - if pathStr == defaultStr || strings.HasPrefix(pathStr, p.BaseURL) { + + baseURL, err := s.Base(n) + if err != nil { + return "", err + } + + // has already been prefixed with base path + if strings.HasPrefix(pathStr, baseURL) { return pathStr, nil } - return path.Join(p.BaseURL, pathStr), nil + return path.Join(baseURL, pathStr), nil +} + +// DefaultLogoPath returns the default logo path for the specified plugin type. +func (s *Service) DefaultLogoPath(pluginType plugins.Type) string { + return path.Join("/", s.cfg.GrafanaAppSubURL, fmt.Sprintf("/public/img/icn-%s.svg", string(pluginType))) } diff --git a/pkg/plugins/manager/loader/assetpath/assetpath_test.go b/pkg/plugins/manager/loader/assetpath/assetpath_test.go index 14da8c824fa..78d32ee75d6 100644 --- a/pkg/plugins/manager/loader/assetpath/assetpath_test.go +++ b/pkg/plugins/manager/loader/assetpath/assetpath_test.go @@ -1,17 +1,20 @@ package assetpath import ( + "net/url" "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/pluginscdn" - "github.com/stretchr/testify/require" ) -func extPath(pluginID string) string { - return "/grafana/data/plugins/" + pluginID +func extPath(pluginID string) *fakes.FakePluginFiles { + return fakes.NewFakePluginFiles(pluginID) } func TestService(t *testing.T) { @@ -33,15 +36,16 @@ func TestService(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - svc := ProvideService(pluginscdn.ProvideService(&config.Cfg{ + cfg := &config.Cfg{ PluginsCDNURLTemplate: tc.cdnBaseURL, PluginSettings: map[string]map[string]string{ "one": {"cdn": "true"}, "two": {}, }, - })) + } + svc := ProvideService(cfg, pluginscdn.ProvideService(cfg)) - const tableOldPath = "/grafana/public/app/plugins/panel/table-old" + tableOldFS := fakes.NewFakePluginFiles("/grafana/public/app/plugins/panel/table-old") jsonData := map[string]plugins.JSONData{ "table-old": {ID: "table-old", Info: plugins.Info{Version: "1.0.0"}}, @@ -56,60 +60,110 @@ func TestService(t *testing.T) { }) t.Run("Base", func(t *testing.T) { - base, err := svc.Base(jsonData["one"], plugins.ClassExternal, extPath("one")) + base, err := svc.Base(NewPluginInfo(jsonData["one"], plugins.ClassExternal, extPath("one"))) require.NoError(t, err) - require.Equal(t, "plugin-cdn/one/1.0.0/public/plugins/one", base) - base, err = svc.Base(jsonData["two"], plugins.ClassExternal, extPath("two")) + u, err := url.JoinPath(tc.cdnBaseURL, "/one/1.0.0/public/plugins/one") require.NoError(t, err) - require.Equal(t, "public/plugins/two", base) + require.Equal(t, u, base) - base, err = svc.Base(jsonData["table-old"], plugins.ClassCore, tableOldPath) + base, err = svc.Base(NewPluginInfo(jsonData["two"], plugins.ClassExternal, extPath("two"))) require.NoError(t, err) - require.Equal(t, "public/app/plugins/table-old", base) + require.Equal(t, "/public/plugins/two", base) + + base, err = svc.Base(NewPluginInfo(jsonData["table-old"], plugins.ClassCore, tableOldFS)) + require.NoError(t, err) + require.Equal(t, "/public/app/plugins/table-old", base) }) t.Run("Module", func(t *testing.T) { - module, err := svc.Module(jsonData["one"], plugins.ClassExternal, extPath("one")) + module, err := svc.Module(NewPluginInfo(jsonData["one"], plugins.ClassExternal, extPath("one"))) require.NoError(t, err) - require.Equal(t, "plugin-cdn/one/1.0.0/public/plugins/one/module", module) - module, err = svc.Module(jsonData["two"], plugins.ClassExternal, extPath("two")) + u, err := url.JoinPath(tc.cdnBaseURL, "/one/1.0.0/public/plugins/one/module.js") require.NoError(t, err) - require.Equal(t, "plugins/two/module", module) + require.Equal(t, u, module) - module, err = svc.Module(jsonData["table-old"], plugins.ClassCore, tableOldPath) + module, err = svc.Module(NewPluginInfo(jsonData["two"], plugins.ClassExternal, extPath("two"))) require.NoError(t, err) - require.Equal(t, "app/plugins/table-old/module", module) + require.Equal(t, "/public/plugins/two/module.js", module) + + module, err = svc.Module(NewPluginInfo(jsonData["table-old"], plugins.ClassCore, tableOldFS)) + require.NoError(t, err) + require.Equal(t, "core:plugin/table-old", module) }) t.Run("RelativeURL", func(t *testing.T) { pluginsMap := map[string]*plugins.Plugin{ "one": { JSONData: plugins.JSONData{ID: "one", Info: plugins.Info{Version: "1.0.0"}}, - BaseURL: "plugin-cdn/one/1.0.0/public/pluginsMap/one", }, "two": { JSONData: plugins.JSONData{ID: "two", Info: plugins.Info{Version: "2.0.0"}}, - BaseURL: "public/pluginsMap/two", }, } - u, err := svc.RelativeURL(pluginsMap["one"], "", "default") - require.NoError(t, err) - require.Equal(t, "default", u) - u, err = svc.RelativeURL(pluginsMap["one"], "path/to/file.txt", "default") + u, err := svc.RelativeURL(NewPluginInfo(pluginsMap["one"].JSONData, plugins.ClassExternal, extPath("one")), "") + require.NoError(t, err) + // given an empty path, base URL will be returned + baseURL, err := svc.Base(NewPluginInfo(pluginsMap["one"].JSONData, plugins.ClassExternal, extPath("one"))) + require.NoError(t, err) + require.Equal(t, baseURL, u) + + u, err = svc.RelativeURL(NewPluginInfo(pluginsMap["one"].JSONData, plugins.ClassExternal, extPath("one")), "path/to/file.txt") require.NoError(t, err) require.Equal(t, strings.TrimRight(tc.cdnBaseURL, "/")+"/one/1.0.0/public/plugins/one/path/to/file.txt", u) - u, err = svc.RelativeURL(pluginsMap["two"], "path/to/file.txt", "default") + u, err = svc.RelativeURL(NewPluginInfo(pluginsMap["two"].JSONData, plugins.ClassExternal, extPath("two")), "path/to/file.txt") require.NoError(t, err) - require.Equal(t, "public/pluginsMap/two/path/to/file.txt", u) + require.Equal(t, "/public/plugins/two/path/to/file.txt", u) - u, err = svc.RelativeURL(pluginsMap["two"], "default", "default") + u, err = svc.RelativeURL(NewPluginInfo(pluginsMap["two"].JSONData, plugins.ClassExternal, extPath("two")), "default") require.NoError(t, err) - require.Equal(t, "default", u) + require.Equal(t, "/public/plugins/two/default", u) }) }) } + + t.Run("With App Sub URL", func(t *testing.T) { + for _, tc := range []struct { + appSubURL string + }{ + { + appSubURL: "grafana", + }, + { + appSubURL: "/grafana", + }, + { + appSubURL: "grafana/", + }, + { + appSubURL: "/grafana/", + }, + } { + cfg := &config.Cfg{GrafanaAppSubURL: tc.appSubURL} + svc := ProvideService(cfg, pluginscdn.ProvideService(cfg)) + + dir := "/plugins/test-datasource" + p := plugins.JSONData{ID: "test-datasource"} + fs := fakes.NewFakePluginFiles(dir) + + base, err := svc.Base(NewPluginInfo(p, plugins.ClassExternal, fs)) + require.NoError(t, err) + require.Equal(t, "/grafana/public/plugins/test-datasource", base) + + mod, err := svc.Module(NewPluginInfo(p, plugins.ClassExternal, fs)) + require.NoError(t, err) + require.Equal(t, "/grafana/public/plugins/test-datasource/module.js", mod) + + base, err = svc.Base(NewPluginInfo(p, plugins.ClassCore, fs)) + require.NoError(t, err) + require.Equal(t, "/grafana/public/app/plugins/test-datasource", base) + + mod, err = svc.Module(NewPluginInfo(p, plugins.ClassCore, fs)) + require.NoError(t, err) + require.Equal(t, "core:plugin/test-datasource", mod) + } + }) } diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 0ceb8c82430..3cb3b18b32c 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -82,8 +82,8 @@ func TestLoader_Load(t *testing.T) { }, Description: "Data source for Amazon AWS monitoring service", Logos: plugins.Logos{ - Small: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", - Large: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", + Small: "/public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", + Large: "/public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", }, }, Includes: []*plugins.Includes{ @@ -105,8 +105,8 @@ func TestLoader_Load(t *testing.T) { Backend: true, QueryOptions: map[string]bool{"minInterval": true}, }, - Module: "app/plugins/datasource/cloudwatch/module", - BaseURL: "public/app/plugins/datasource/cloudwatch", + Module: "core:plugin/cloudwatch", + BaseURL: "/public/app/plugins/datasource/cloudwatch", FS: mustNewStaticFSForTests(t, filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch")), Signature: plugins.SignatureStatusInternal, @@ -132,8 +132,8 @@ func TestLoader_Load(t *testing.T) { }, Version: "1.0.0", Logos: plugins.Logos{ - Small: "public/img/icn-datasource.svg", - Large: "public/img/icn-datasource.svg", + Small: "/public/img/icn-datasource.svg", + Large: "/public/img/icn-datasource.svg", }, Description: "Test", }, @@ -145,8 +145,8 @@ func TestLoader_Load(t *testing.T) { Backend: true, State: "alpha", }, - Module: "plugins/test-datasource/module", - BaseURL: "public/plugins/test-datasource", + Module: "/public/plugins/test-datasource/module.js", + BaseURL: "/public/plugins/test-datasource", FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/valid-v2-signature/plugin/")), Signature: "valid", SignatureType: plugins.SignatureTypeGrafana, @@ -171,8 +171,8 @@ func TestLoader_Load(t *testing.T) { URL: "http://test.com", }, Logos: plugins.Logos{ - Small: "public/plugins/test-app/img/logo_small.png", - Large: "public/plugins/test-app/img/logo_large.png", + Small: "/public/plugins/test-app/img/logo_small.png", + Large: "/public/plugins/test-app/img/logo_large.png", }, Links: []plugins.InfoLink{ {Name: "Project site", URL: "http://project.com"}, @@ -180,8 +180,8 @@ func TestLoader_Load(t *testing.T) { }, Description: "Official Grafana Test App & Dashboard bundle", Screenshots: []plugins.Screenshots{ - {Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"}, - {Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"}, + {Path: "/public/plugins/test-app/img/screenshot1.png", Name: "img1"}, + {Path: "/public/plugins/test-app/img/screenshot2.png", Name: "img2"}, }, Version: "1.0.0", Updated: "2015-02-10", @@ -222,8 +222,8 @@ func TestLoader_Load(t *testing.T) { }, }, Class: plugins.ClassExternal, - Module: "plugins/test-app/module", - BaseURL: "public/plugins/test-app", + Module: "/public/plugins/test-app/module.js", + BaseURL: "/public/plugins/test-app", FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/includes-symlinks")), Signature: "valid", SignatureType: plugins.SignatureTypeGrafana, @@ -249,8 +249,8 @@ func TestLoader_Load(t *testing.T) { URL: "https://grafana.com", }, Logos: plugins.Logos{ - Small: "public/img/icn-datasource.svg", - Large: "public/img/icn-datasource.svg", + Small: "/public/img/icn-datasource.svg", + Large: "/public/img/icn-datasource.svg", }, Description: "Test", }, @@ -262,8 +262,8 @@ func TestLoader_Load(t *testing.T) { State: plugins.ReleaseStateAlpha, }, Class: plugins.ClassExternal, - Module: "plugins/test-datasource/module", - BaseURL: "public/plugins/test-datasource", + Module: "/public/plugins/test-datasource/module.js", + BaseURL: "/public/plugins/test-datasource", FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/unsigned-datasource/plugin")), Signature: "unsigned", }, @@ -295,8 +295,8 @@ func TestLoader_Load(t *testing.T) { URL: "https://grafana.com", }, Logos: plugins.Logos{ - Small: "public/img/icn-datasource.svg", - Large: "public/img/icn-datasource.svg", + Small: "/public/img/icn-datasource.svg", + Large: "/public/img/icn-datasource.svg", }, Description: "Test", }, @@ -308,8 +308,8 @@ func TestLoader_Load(t *testing.T) { State: plugins.ReleaseStateAlpha, }, Class: plugins.ClassExternal, - Module: "plugins/test-datasource/module", - BaseURL: "public/plugins/test-datasource", + Module: "/public/plugins/test-datasource/module.js", + BaseURL: "/public/plugins/test-datasource", FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/unsigned-datasource/plugin")), Signature: plugins.SignatureStatusUnsigned, }, @@ -374,8 +374,8 @@ func TestLoader_Load(t *testing.T) { {Name: "License & Terms", URL: "http://license.com"}, }, Logos: plugins.Logos{ - Small: "public/img/icn-app.svg", - Large: "public/img/icn-app.svg", + Small: "/public/img/icn-app.svg", + Large: "/public/img/icn-app.svg", }, Updated: "2015-02-10", }, @@ -394,8 +394,48 @@ func TestLoader_Load(t *testing.T) { FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/test-app-with-includes")), Class: plugins.ClassExternal, Signature: plugins.SignatureStatusUnsigned, - Module: "plugins/test-app/module", - BaseURL: "public/plugins/test-app", + Module: "/public/plugins/test-app/module.js", + BaseURL: "/public/plugins/test-app", + }, + }, + }, + { + name: "Load a plugin with app sub url set", + class: plugins.ClassExternal, + cfg: &config.Cfg{ + DevMode: true, + GrafanaAppSubURL: "grafana", + }, + pluginPaths: []string{"../testdata/unsigned-datasource"}, + want: []*plugins.Plugin{ + { + JSONData: plugins.JSONData{ + ID: "test-datasource", + Type: plugins.TypeDataSource, + Name: "Test", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Grafana Labs", + URL: "https://grafana.com", + }, + Logos: plugins.Logos{ + Small: "/grafana/public/img/icn-datasource.svg", + Large: "/grafana/public/img/icn-datasource.svg", + }, + Description: "Test", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "*", + Plugins: []plugins.Dependency{}, + }, + Backend: true, + State: plugins.ReleaseStateAlpha, + }, + Class: plugins.ClassExternal, + Module: "/grafana/public/plugins/test-datasource/module.js", + BaseURL: "/grafana/public/plugins/test-datasource", + FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/unsigned-datasource/plugin")), + Signature: plugins.SignatureStatusUnsigned, }, }, }, diff --git a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go index 4340b400557..19300c4e247 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go +++ b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go @@ -48,7 +48,7 @@ func New(cfg *config.Cfg, opts Opts) *Bootstrap { } if opts.DecorateFuncs == nil { - opts.DecorateFuncs = DefaultDecorateFuncs + opts.DecorateFuncs = DefaultDecorateFuncs(cfg) } return &Bootstrap{ diff --git a/pkg/plugins/manager/pipeline/bootstrap/factory.go b/pkg/plugins/manager/pipeline/bootstrap/factory.go index 5e179b1f31c..db966834ba8 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/factory.go +++ b/pkg/plugins/manager/pipeline/bootstrap/factory.go @@ -25,27 +25,27 @@ func NewDefaultPluginFactory(assetPath *assetpath.Service) *DefaultPluginFactory func (f *DefaultPluginFactory) createPlugin(p plugins.FoundPlugin, class plugins.Class, sig plugins.Signature) (*plugins.Plugin, error) { - baseURL, err := f.assetPath.Base(p.JSONData, class, p.FS.Base()) + info := assetpath.NewPluginInfo(p.JSONData, class, p.FS) + baseURL, err := f.assetPath.Base(info) if err != nil { return nil, fmt.Errorf("base url: %w", err) } - moduleURL, err := f.assetPath.Module(p.JSONData, class, p.FS.Base()) + moduleURL, err := f.assetPath.Module(info) if err != nil { return nil, fmt.Errorf("module url: %w", err) } - plugin := &plugins.Plugin{ JSONData: p.JSONData, + Class: class, FS: p.FS, BaseURL: baseURL, Module: moduleURL, - Class: class, Signature: sig.Status, SignatureType: sig.Type, SignatureOrg: sig.SigningOrg, } - plugin.SetLogger(log.New(fmt.Sprintf("plugin.%s", plugin.ID))) + plugin.SetLogger(log.New(fmt.Sprintf("plugin.%s", plugin.ID))) if err = setImages(plugin, f.assetPath); err != nil { return nil, err } @@ -54,23 +54,25 @@ func (f *DefaultPluginFactory) createPlugin(p plugins.FoundPlugin, class plugins } func setImages(p *plugins.Plugin, assetPath *assetpath.Service) error { + info := assetpath.NewPluginInfo(p.JSONData, p.Class, p.FS) var err error for _, dst := range []*string{&p.Info.Logos.Small, &p.Info.Logos.Large} { - *dst, err = assetPath.RelativeURL(p, *dst, defaultLogoPath(p.Type)) + if len(*dst) == 0 { + *dst = assetPath.DefaultLogoPath(p.Type) + continue + } + + *dst, err = assetPath.RelativeURL(info, *dst) if err != nil { return fmt.Errorf("logo: %w", err) } } for i := 0; i < len(p.Info.Screenshots); i++ { screenshot := &p.Info.Screenshots[i] - screenshot.Path, err = assetPath.RelativeURL(p, screenshot.Path, "") + screenshot.Path, err = assetPath.RelativeURL(info, screenshot.Path) if err != nil { return fmt.Errorf("screenshot %d relative url: %w", i, err) } } return nil } - -func defaultLogoPath(pluginType plugins.Type) string { - return fmt.Sprintf("public/img/icn-%s.svg", string(pluginType)) -} diff --git a/pkg/plugins/manager/pipeline/bootstrap/steps.go b/pkg/plugins/manager/pipeline/bootstrap/steps.go index 7cd475f0f5d..624c83ba406 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/steps.go +++ b/pkg/plugins/manager/pipeline/bootstrap/steps.go @@ -7,9 +7,9 @@ import ( "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" - "github.com/grafana/grafana/pkg/util" ) // DefaultConstructor implements the default ConstructFunc used for the Construct step of the Bootstrap stage. @@ -27,10 +27,12 @@ func DefaultConstructFunc(signatureCalculator plugins.SignatureCalculator, asset } // DefaultDecorateFuncs are the default DecorateFuncs used for the Decorate step of the Bootstrap stage. -var DefaultDecorateFuncs = []DecorateFunc{ - AliasDecorateFunc, - AppDefaultNavURLDecorateFunc, - AppChildDecorateFunc, +func DefaultDecorateFuncs(cfg *config.Cfg) []DecorateFunc { + return []DecorateFunc{ + AliasDecorateFunc, + AppDefaultNavURLDecorateFunc, + AppChildDecorateFunc(cfg), + } } // NewDefaultConstructor returns a new DefaultConstructor. @@ -123,24 +125,26 @@ func setDefaultNavURL(p *plugins.Plugin) { } // AppChildDecorateFunc is a DecorateFunc that configures child plugins of app plugins. -func AppChildDecorateFunc(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { - if p.Parent != nil && p.Parent.IsApp() { - configureAppChildPlugin(p.Parent, p) +func AppChildDecorateFunc(cfg *config.Cfg) DecorateFunc { + return func(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { + if p.Parent != nil && p.Parent.IsApp() { + configureAppChildPlugin(cfg, p.Parent, p) + } + return p, nil } - return p, nil } -func configureAppChildPlugin(parent *plugins.Plugin, child *plugins.Plugin) { +func configureAppChildPlugin(cfg *config.Cfg, parent *plugins.Plugin, child *plugins.Plugin) { if !parent.IsApp() { return } - appSubPath := strings.ReplaceAll(strings.Replace(child.FS.Base(), parent.FS.Base(), "", 1), "\\", "/") child.IncludedInAppID = parent.ID child.BaseURL = parent.BaseURL + appSubPath := strings.ReplaceAll(strings.Replace(child.FS.Base(), parent.FS.Base(), "", 1), "\\", "/") if parent.IsCorePlugin() { - child.Module = util.JoinURLFragments("app/plugins/app/"+parent.ID, appSubPath) + "/module" + child.Module = path.Join("core:plugin", parent.ID, appSubPath) } else { - child.Module = util.JoinURLFragments("plugins/"+parent.ID, appSubPath) + "/module" + child.Module = path.Join("/", cfg.GrafanaAppSubURL, "/public/plugins", parent.ID, appSubPath, "module.js") } } diff --git a/pkg/plugins/manager/pipeline/bootstrap/steps_test.go b/pkg/plugins/manager/pipeline/bootstrap/steps_test.go index f22ed0478b1..670a667a707 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/steps_test.go +++ b/pkg/plugins/manager/pipeline/bootstrap/steps_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/fakes" ) @@ -65,7 +66,7 @@ func TestSetDefaultNavURL(t *testing.T) { }) } -func TestSetPathsBasedOnApp(t *testing.T) { +func Test_configureAppChildPlugin(t *testing.T) { t.Run("When setting paths based on core plugin on Windows", func(t *testing.T) { child := &plugins.Plugin{ FS: fakes.NewFakePluginFiles("c:\\grafana\\public\\app\\plugins\\app\\testdata-app\\datasources\\datasource"), @@ -77,13 +78,42 @@ func TestSetPathsBasedOnApp(t *testing.T) { }, Class: plugins.ClassCore, FS: fakes.NewFakePluginFiles("c:\\grafana\\public\\app\\plugins\\app\\testdata-app"), - BaseURL: "public/app/plugins/app/testdata-app", + BaseURL: "/public/app/plugins/app/testdata-app", } - configureAppChildPlugin(parent, child) + configureAppChildPlugin(&config.Cfg{}, parent, child) - require.Equal(t, "app/plugins/app/testdata-app/datasources/datasource/module", child.Module) + require.Equal(t, "core:plugin/testdata-app/datasources/datasource", child.Module) require.Equal(t, "testdata-app", child.IncludedInAppID) - require.Equal(t, "public/app/plugins/app/testdata-app", child.BaseURL) + require.Equal(t, "/public/app/plugins/app/testdata-app", child.BaseURL) + + t.Run("App sub URL has no effect on Core plugins", func(t *testing.T) { + configureAppChildPlugin(&config.Cfg{GrafanaAppSubURL: "/grafana"}, parent, child) + + require.Equal(t, "core:plugin/testdata-app/datasources/datasource", child.Module) + require.Equal(t, "testdata-app", child.IncludedInAppID) + require.Equal(t, "/public/app/plugins/app/testdata-app", child.BaseURL) + }) + }) + + t.Run("When setting paths based on external plugin with app sub URL", func(t *testing.T) { + child := &plugins.Plugin{ + FS: fakes.NewFakePluginFiles("/plugins/parent-app/child-panel"), + } + parent := &plugins.Plugin{ + JSONData: plugins.JSONData{ + Type: plugins.TypeApp, + ID: "testdata-app", + }, + Class: plugins.ClassExternal, + FS: fakes.NewFakePluginFiles("/plugins/parent-app"), + BaseURL: "/grafana/plugins/parent-app", + } + + configureAppChildPlugin(&config.Cfg{GrafanaAppSubURL: "/grafana"}, parent, child) + + require.Equal(t, "/grafana/public/plugins/testdata-app/child-panel/module.js", child.Module) + require.Equal(t, "testdata-app", child.IncludedInAppID) + require.Equal(t, "/grafana/plugins/parent-app", child.BaseURL) }) } diff --git a/pkg/plugins/pluginscdn/pluginscdn.go b/pkg/plugins/pluginscdn/pluginscdn.go index da21336523b..5f93f482598 100644 --- a/pkg/plugins/pluginscdn/pluginscdn.go +++ b/pkg/plugins/pluginscdn/pluginscdn.go @@ -10,9 +10,6 @@ import ( const ( // cdnAssetPathTemplate is the relative path template used to locate plugin CDN assets cdnAssetPathTemplate = "{id}/{version}/public/plugins/{id}/{assetPath}" - - // systemJSCDNURLTemplate is a special path template used by system.js to identify plugin CDN assets - systemJSCDNURLTemplate = "plugin-cdn/" + cdnAssetPathTemplate ) var ErrPluginNotCDN = errors.New("plugin is not a cdn plugin") @@ -57,21 +54,6 @@ func (s *Service) BaseURL() (string, error) { return s.cfg.PluginsCDNURLTemplate, nil } -// SystemJSAssetPath returns a system-js path for the specified asset on the plugins CDN. -// The returned path will follow the template specified in systemJSCDNURLTemplate. -// If assetPath is an empty string, the base path for the plugin is returned. -func (s *Service) SystemJSAssetPath(pluginID, pluginVersion, assetPath string) (string, error) { - u, err := URLConstructor{ - cdnURLTemplate: systemJSCDNURLTemplate, - pluginID: pluginID, - pluginVersion: pluginVersion, - }.Path(assetPath) - if err != nil { - return "", err - } - return u.String(), nil -} - // AssetURL returns the URL of a CDN asset for a CDN plugin. If the specified plugin is not a CDN plugin, // it returns ErrPluginNotCDN. func (s *Service) AssetURL(pluginID, pluginVersion, assetPath string) (string, error) { diff --git a/pkg/services/pluginsintegration/config/config.go b/pkg/services/pluginsintegration/config/config.go index 8b76dbcb3de..590b0e7826c 100644 --- a/pkg/services/pluginsintegration/config/config.go +++ b/pkg/services/pluginsintegration/config/config.go @@ -41,6 +41,7 @@ func ProvideConfig(settingProvider setting.Provider, grafanaCfg *setting.Cfg, fe grafanaCfg.PluginLogBackendRequests, grafanaCfg.PluginsCDNURLTemplate, grafanaCfg.AppURL, + grafanaCfg.AppSubURL, tracingCfg, features, grafanaCfg.AngularSupportEnabled, diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index 29e79b5c812..c9694976b6e 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -83,8 +83,8 @@ func TestLoader_Load(t *testing.T) { }, Description: "Data source for Amazon AWS monitoring service", Logos: plugins.Logos{ - Small: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", - Large: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", + Small: "/public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", + Large: "/public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", }, }, Includes: []*plugins.Includes{ @@ -106,8 +106,8 @@ func TestLoader_Load(t *testing.T) { Backend: true, QueryOptions: map[string]bool{"minInterval": true}, }, - Module: "app/plugins/datasource/cloudwatch/module", - BaseURL: "public/app/plugins/datasource/cloudwatch", + Module: "core:plugin/cloudwatch", + BaseURL: "/public/app/plugins/datasource/cloudwatch", FS: mustNewStaticFSForTests(t, filepath.Join(corePluginDir(t), "app/plugins/datasource/cloudwatch")), Signature: plugins.SignatureStatusInternal, @@ -133,8 +133,8 @@ func TestLoader_Load(t *testing.T) { }, Version: "1.0.0", Logos: plugins.Logos{ - Small: "public/img/icn-datasource.svg", - Large: "public/img/icn-datasource.svg", + Small: "/public/img/icn-datasource.svg", + Large: "/public/img/icn-datasource.svg", }, Description: "Test", }, @@ -146,8 +146,8 @@ func TestLoader_Load(t *testing.T) { Backend: true, State: "alpha", }, - Module: "plugins/test-datasource/module", - BaseURL: "public/plugins/test-datasource", + Module: "/public/plugins/test-datasource/module.js", + BaseURL: "/public/plugins/test-datasource", FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-signature/plugin/")), Signature: "valid", SignatureType: plugins.SignatureTypeGrafana, @@ -172,8 +172,8 @@ func TestLoader_Load(t *testing.T) { URL: "http://test.com", }, Logos: plugins.Logos{ - Small: "public/plugins/test-app/img/logo_small.png", - Large: "public/plugins/test-app/img/logo_large.png", + Small: "/public/plugins/test-app/img/logo_small.png", + Large: "/public/plugins/test-app/img/logo_large.png", }, Links: []plugins.InfoLink{ {Name: "Project site", URL: "http://project.com"}, @@ -181,8 +181,8 @@ func TestLoader_Load(t *testing.T) { }, Description: "Official Grafana Test App & Dashboard bundle", Screenshots: []plugins.Screenshots{ - {Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"}, - {Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"}, + {Path: "/public/plugins/test-app/img/screenshot1.png", Name: "img1"}, + {Path: "/public/plugins/test-app/img/screenshot2.png", Name: "img2"}, }, Version: "1.0.0", Updated: "2015-02-10", @@ -223,8 +223,8 @@ func TestLoader_Load(t *testing.T) { }, }, Class: plugins.ClassExternal, - Module: "plugins/test-app/module", - BaseURL: "public/plugins/test-app", + Module: "/public/plugins/test-app/module.js", + BaseURL: "/public/plugins/test-app", FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "includes-symlinks")), Signature: "valid", SignatureType: plugins.SignatureTypeGrafana, @@ -250,8 +250,8 @@ func TestLoader_Load(t *testing.T) { URL: "https://grafana.com", }, Logos: plugins.Logos{ - Small: "public/img/icn-datasource.svg", - Large: "public/img/icn-datasource.svg", + Small: "/public/img/icn-datasource.svg", + Large: "/public/img/icn-datasource.svg", }, Description: "Test", }, @@ -263,8 +263,8 @@ func TestLoader_Load(t *testing.T) { State: plugins.ReleaseStateAlpha, }, Class: plugins.ClassExternal, - Module: "plugins/test-datasource/module", - BaseURL: "public/plugins/test-datasource", + Module: "/public/plugins/test-datasource/module.js", + BaseURL: "/public/plugins/test-datasource", FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "unsigned-datasource/plugin")), Signature: "unsigned", }, @@ -301,8 +301,8 @@ func TestLoader_Load(t *testing.T) { URL: "https://grafana.com", }, Logos: plugins.Logos{ - Small: "public/img/icn-datasource.svg", - Large: "public/img/icn-datasource.svg", + Small: "/public/img/icn-datasource.svg", + Large: "/public/img/icn-datasource.svg", }, Description: "Test", }, @@ -314,8 +314,8 @@ func TestLoader_Load(t *testing.T) { State: plugins.ReleaseStateAlpha, }, Class: plugins.ClassExternal, - Module: "plugins/test-datasource/module", - BaseURL: "public/plugins/test-datasource", + Module: "/public/plugins/test-datasource/module.js", + BaseURL: "/public/plugins/test-datasource", FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "unsigned-datasource/plugin")), Signature: plugins.SignatureStatusUnsigned, }, @@ -403,8 +403,8 @@ func TestLoader_Load(t *testing.T) { {Name: "License & Terms", URL: "http://license.com"}, }, Logos: plugins.Logos{ - Small: "public/img/icn-app.svg", - Large: "public/img/icn-app.svg", + Small: "/public/img/icn-app.svg", + Large: "/public/img/icn-app.svg", }, Updated: "2015-02-10", }, @@ -423,8 +423,48 @@ func TestLoader_Load(t *testing.T) { FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app-with-includes")), Class: plugins.ClassExternal, Signature: plugins.SignatureStatusUnsigned, - Module: "plugins/test-app/module", - BaseURL: "public/plugins/test-app", + Module: "/public/plugins/test-app/module.js", + BaseURL: "/public/plugins/test-app", + }, + }, + }, + { + name: "Load a plugin with app sub url set", + class: plugins.ClassExternal, + cfg: &config.Cfg{ + DevMode: true, + GrafanaAppSubURL: "grafana", + }, + pluginPaths: []string{filepath.Join(testDataDir(t), "unsigned-datasource")}, + want: []*plugins.Plugin{ + { + JSONData: plugins.JSONData{ + ID: "test-datasource", + Type: plugins.TypeDataSource, + Name: "Test", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Grafana Labs", + URL: "https://grafana.com", + }, + Logos: plugins.Logos{ + Small: "/grafana/public/img/icn-datasource.svg", + Large: "/grafana/public/img/icn-datasource.svg", + }, + Description: "Test", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "*", + Plugins: []plugins.Dependency{}, + }, + Backend: true, + State: plugins.ReleaseStateAlpha, + }, + Class: plugins.ClassExternal, + Module: "/grafana/public/plugins/test-datasource/module.js", + BaseURL: "/grafana/public/plugins/test-datasource", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "unsigned-datasource/plugin")), + Signature: plugins.SignatureStatusUnsigned, }, }, }, @@ -478,8 +518,8 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) { }, Version: "1.0.0", Logos: plugins.Logos{ - Small: "public/plugins/grafana-test-datasource/img/ds.svg", - Large: "public/plugins/grafana-test-datasource/img/ds.svg", + Small: "/public/plugins/grafana-test-datasource/img/ds.svg", + Large: "/public/plugins/grafana-test-datasource/img/ds.svg", }, Updated: "2023-08-03", Screenshots: []plugins.Screenshots{}, @@ -513,8 +553,8 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) { FS: mustNewStaticFSForTests(t, pluginPaths[0]), Class: plugins.ClassExternal, Signature: plugins.SignatureStatusUnsigned, - Module: "plugins/grafana-test-datasource/module", - BaseURL: "public/plugins/grafana-test-datasource", + Module: "/public/plugins/grafana-test-datasource/module.js", + BaseURL: "/public/plugins/grafana-test-datasource", ExternalService: &oauth.ExternalService{ ClientID: "client-id", ClientSecret: "secretz", @@ -613,8 +653,8 @@ func TestLoader_Load_CustomSource(t *testing.T) { FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "cdn/plugin")), Class: plugins.ClassBundled, Signature: plugins.SignatureStatusValid, - BaseURL: "plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel", - Module: "plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module", + BaseURL: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel", + Module: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module.js", }} l := newLoader(t, cfg, fakes.NewFakePluginRegistry(), fakes.NewFakeProcessManager(), fakes.NewFakeBackendProcessProvider(), newFakeSignatureErrorTracker()) @@ -671,8 +711,8 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { URL: "https://willbrowne.com", }, Logos: plugins.Logos{ - Small: "public/img/icn-datasource.svg", - Large: "public/img/icn-datasource.svg", + Small: "/public/img/icn-datasource.svg", + Large: "/public/img/icn-datasource.svg", }, Description: "Test", Version: "1.0.0", @@ -686,8 +726,8 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { State: plugins.ReleaseStateAlpha, }, Class: plugins.ClassExternal, - Module: "plugins/test-datasource/module", - BaseURL: "public/plugins/test-datasource", + Module: "/public/plugins/test-datasource/module.js", + BaseURL: "/public/plugins/test-datasource", FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature/plugin")), Signature: "valid", SignatureType: plugins.SignatureTypePrivate, @@ -766,8 +806,8 @@ func TestLoader_Load_RBACReady(t *testing.T) { Version: "1.0.0", Links: []plugins.InfoLink{}, Logos: plugins.Logos{ - Small: "public/img/icn-app.svg", - Large: "public/img/icn-app.svg", + Small: "/public/img/icn-app.svg", + Large: "/public/img/icn-app.svg", }, Updated: "2015-02-10", }, @@ -798,8 +838,8 @@ func TestLoader_Load_RBACReady(t *testing.T) { Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypePrivate, SignatureOrg: "gabrielmabille", - Module: "plugins/test-app/module", - BaseURL: "public/plugins/test-app", + Module: "/public/plugins/test-app/module.js", + BaseURL: "/public/plugins/test-app", }, }, }, @@ -843,8 +883,8 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) { Author: plugins.InfoLink{Name: "Will Browne", URL: "https://willbrowne.com"}, Description: "Test", Logos: plugins.Logos{ - Small: "public/img/icn-datasource.svg", - Large: "public/img/icn-datasource.svg", + Small: "/public/img/icn-datasource.svg", + Large: "/public/img/icn-datasource.svg", }, Version: "1.0.0", }, @@ -858,8 +898,8 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) { Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypePrivate, SignatureOrg: "Will Browne", - Module: "plugins/test-datasource/module", - BaseURL: "public/plugins/test-datasource", + Module: "/public/plugins/test-datasource/module.js", + BaseURL: "/public/plugins/test-datasource", }, } @@ -905,12 +945,12 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) { {Name: "License & Terms", URL: "http://license.com"}, }, Logos: plugins.Logos{ - Small: "public/plugins/test-app/img/logo_small.png", - Large: "public/plugins/test-app/img/logo_large.png", + Small: "/public/plugins/test-app/img/logo_small.png", + Large: "/public/plugins/test-app/img/logo_large.png", }, Screenshots: []plugins.Screenshots{ - {Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"}, - {Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"}, + {Path: "/public/plugins/test-app/img/screenshot1.png", Name: "img1"}, + {Path: "/public/plugins/test-app/img/screenshot2.png", Name: "img2"}, }, Updated: "2015-02-10", }, @@ -934,8 +974,8 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) { Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", - Module: "plugins/test-app/module", - BaseURL: "public/plugins/test-app", + Module: "/public/plugins/test-app/module.js", + BaseURL: "/public/plugins/test-app", }, } @@ -985,12 +1025,12 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) { {Name: "License & Terms", URL: "http://license.com"}, }, Logos: plugins.Logos{ - Small: "public/plugins/test-app/img/logo_small.png", - Large: "public/plugins/test-app/img/logo_large.png", + Small: "/public/plugins/test-app/img/logo_small.png", + Large: "/public/plugins/test-app/img/logo_large.png", }, Screenshots: []plugins.Screenshots{ - {Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"}, - {Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"}, + {Path: "/public/plugins/test-app/img/screenshot1.png", Name: "img1"}, + {Path: "/public/plugins/test-app/img/screenshot2.png", Name: "img2"}, }, Updated: "2015-02-10", }, @@ -1014,8 +1054,8 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) { Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", - Module: "plugins/test-app/module", - BaseURL: "public/plugins/test-app", + Module: "/public/plugins/test-app/module.js", + BaseURL: "/public/plugins/test-app", }, } @@ -1165,8 +1205,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { URL: "http://grafana.com", }, Logos: plugins.Logos{ - Small: "public/img/icn-datasource.svg", - Large: "public/img/icn-datasource.svg", + Small: "/public/img/icn-datasource.svg", + Large: "/public/img/icn-datasource.svg", }, Description: "Parent plugin", Version: "1.0.0", @@ -1178,8 +1218,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }, Backend: true, }, - Module: "plugins/test-datasource/module", - BaseURL: "public/plugins/test-datasource", + Module: "/public/plugins/test-datasource/module.js", + BaseURL: "/public/plugins/test-datasource", FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent")), Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, @@ -1198,8 +1238,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { URL: "http://grafana.com", }, Logos: plugins.Logos{ - Small: "public/img/icn-panel.svg", - Large: "public/img/icn-panel.svg", + Small: "/public/img/icn-panel.svg", + Large: "/public/img/icn-panel.svg", }, Description: "Child plugin", Version: "1.0.1", @@ -1210,8 +1250,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { Plugins: []plugins.Dependency{}, }, }, - Module: "plugins/test-panel/module", - BaseURL: "public/plugins/test-panel", + Module: "/public/plugins/test-panel/module.js", + BaseURL: "/public/plugins/test-panel", FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent/nested")), Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, @@ -1290,8 +1330,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { {Name: "License", URL: "https://github.com/grafana/grafana-starter-app/blob/master/LICENSE"}, }, Logos: plugins.Logos{ - Small: "public/plugins/myorgid-simple-app/img/logo.svg", - Large: "public/plugins/myorgid-simple-app/img/logo.svg", + Small: "/public/plugins/myorgid-simple-app/img/logo.svg", + Large: "/public/plugins/myorgid-simple-app/img/logo.svg", }, Screenshots: []plugins.Screenshots{}, Description: "Grafana App Plugin Template", @@ -1346,8 +1386,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }, Backend: false, }, - Module: "plugins/myorgid-simple-app/module", - BaseURL: "public/plugins/myorgid-simple-app", + Module: "/public/plugins/myorgid-simple-app/module.js", + BaseURL: "/public/plugins/myorgid-simple-app", FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist")), DefaultNavURL: "/plugins/myorgid-simple-app/page/root-page-react", Signature: plugins.SignatureStatusValid, @@ -1370,8 +1410,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { {Name: "License", URL: "https://github.com/grafana/grafana-starter-panel/blob/master/LICENSE"}, }, Logos: plugins.Logos{ - Small: "public/plugins/myorgid-simple-panel/img/logo.svg", - Large: "public/plugins/myorgid-simple-panel/img/logo.svg", + Small: "/public/plugins/myorgid-simple-panel/img/logo.svg", + Large: "/public/plugins/myorgid-simple-panel/img/logo.svg", }, Screenshots: []plugins.Screenshots{}, Description: "Grafana Panel Plugin Template", @@ -1384,8 +1424,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { Plugins: []plugins.Dependency{}, }, }, - Module: "plugins/myorgid-simple-app/child/module", - BaseURL: "public/plugins/myorgid-simple-app", + Module: "/public/plugins/myorgid-simple-app/child/module.js", + BaseURL: "/public/plugins/myorgid-simple-app", FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist/child")), IncludedInAppID: parent.ID, Signature: plugins.SignatureStatusValid, @@ -1434,7 +1474,7 @@ type loaderDepOpts struct { func newLoader(t *testing.T, cfg *config.Cfg, reg registry.Service, proc process.Manager, backendFactory plugins.BackendFactoryProvider, sigErrTracker pluginerrs.SignatureErrorTracker) *Loader { - assets := assetpath.ProvideService(pluginscdn.ProvideService(cfg)) + assets := assetpath.ProvideService(cfg, pluginscdn.ProvideService(cfg)) lic := fakes.NewFakeLicensingService() angularInspector := angularinspector.NewStaticInspector() @@ -1449,7 +1489,7 @@ func newLoader(t *testing.T, cfg *config.Cfg, reg registry.Service, proc process } func newLoaderWithOpts(t *testing.T, cfg *config.Cfg, opts loaderDepOpts) *Loader { - assets := assetpath.ProvideService(pluginscdn.ProvideService(cfg)) + assets := assetpath.ProvideService(cfg, pluginscdn.ProvideService(cfg)) lic := fakes.NewFakeLicensingService() reg := fakes.NewFakePluginRegistry() proc := fakes.NewFakeProcessManager() diff --git a/pkg/services/pluginsintegration/pipeline/pipeline.go b/pkg/services/pluginsintegration/pipeline/pipeline.go index 2bb0215acc3..86fac1193a8 100644 --- a/pkg/services/pluginsintegration/pipeline/pipeline.go +++ b/pkg/services/pluginsintegration/pipeline/pipeline.go @@ -37,7 +37,7 @@ func ProvideDiscoveryStage(cfg *config.Cfg, pf finder.Finder, pr registry.Servic func ProvideBootstrapStage(cfg *config.Cfg, sc plugins.SignatureCalculator, a *assetpath.Service) *bootstrap.Bootstrap { return bootstrap.New(cfg, bootstrap.Opts{ ConstructFunc: bootstrap.DefaultConstructFunc(sc, a), - DecorateFuncs: bootstrap.DefaultDecorateFuncs, + DecorateFuncs: bootstrap.DefaultDecorateFuncs(cfg), }) } diff --git a/pkg/services/pluginsintegration/test_helper.go b/pkg/services/pluginsintegration/test_helper.go index 6df9775f30c..bbb674118e0 100644 --- a/pkg/services/pluginsintegration/test_helper.go +++ b/pkg/services/pluginsintegration/test_helper.go @@ -52,7 +52,7 @@ func CreateIntegrationTestCtx(t *testing.T, cfg *setting.Cfg, coreRegistry *core errTracker := pluginerrs.ProvideSignatureErrorTracker() disc := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(true), reg) - boot := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(cdn)) + boot := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pCfg, cdn)) valid := pipeline.ProvideValidationStage(pCfg, signature.NewValidator(signature.NewUnsignedAuthorizer(pCfg)), angularInspector, errTracker) init := pipeline.ProvideInitializationStage(pCfg, reg, fakes.NewFakeLicensingService(), provider.ProvideService(coreRegistry), proc, &fakes.FakeOauthService{}, fakes.NewFakeRoleRegistry()) term, err := pipeline.ProvideTerminationStage(pCfg, reg, proc) @@ -90,7 +90,7 @@ func CreateTestLoader(t *testing.T, cfg *pluginsCfg.Cfg, opts LoaderOpts) *loade } if opts.Bootstrapper == nil { - opts.Bootstrapper = pipeline.ProvideBootstrapStage(cfg, signature.ProvideService(cfg, statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(cfg))) + opts.Bootstrapper = pipeline.ProvideBootstrapStage(cfg, signature.ProvideService(cfg, statickey.New()), assetpath.ProvideService(cfg, pluginscdn.ProvideService(cfg))) } if opts.Validator == nil { diff --git a/pkg/tests/api/plugins/data/expectedListResp.json b/pkg/tests/api/plugins/data/expectedListResp.json index 279e9d5cc60..0b2efa20f1e 100644 --- a/pkg/tests/api/plugins/data/expectedListResp.json +++ b/pkg/tests/api/plugins/data/expectedListResp.json @@ -13,8 +13,8 @@ "description": "Shows list of alerts and their current status", "links": null, "logos": { - "small": "public/app/plugins/panel/alertlist/img/icn-singlestat-panel.svg", - "large": "public/app/plugins/panel/alertlist/img/icn-singlestat-panel.svg" + "small": "/public/app/plugins/panel/alertlist/img/icn-singlestat-panel.svg", + "large": "/public/app/plugins/panel/alertlist/img/icn-singlestat-panel.svg" }, "build": {}, "screenshots": null, @@ -55,8 +55,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/alertmanager/img/logo.svg", - "large": "public/app/plugins/datasource/alertmanager/img/logo.svg" + "small": "/public/app/plugins/datasource/alertmanager/img/logo.svg", + "large": "/public/app/plugins/datasource/alertmanager/img/logo.svg" }, "build": {}, "screenshots": null, @@ -92,8 +92,8 @@ "description": "List annotations", "links": null, "logos": { - "small": "public/app/plugins/panel/annolist/img/icn-annolist-panel.svg", - "large": "public/app/plugins/panel/annolist/img/icn-annolist-panel.svg" + "small": "/public/app/plugins/panel/annolist/img/icn-annolist-panel.svg", + "large": "/public/app/plugins/panel/annolist/img/icn-annolist-panel.svg" }, "build": {}, "screenshots": null, @@ -138,22 +138,22 @@ } ], "logos": { - "small": "public/app/plugins/datasource/azuremonitor/img/logo.jpg", - "large": "public/app/plugins/datasource/azuremonitor/img/logo.jpg" + "small": "/public/app/plugins/datasource/azuremonitor/img/logo.jpg", + "large": "/public/app/plugins/datasource/azuremonitor/img/logo.jpg" }, "build": {}, "screenshots": [ { "name": "Azure Contoso Loans", - "path": "public/app/plugins/datasource/azuremonitor/img/contoso_loans_grafana_dashboard.png" + "path": "/public/app/plugins/datasource/azuremonitor/img/contoso_loans_grafana_dashboard.png" }, { "name": "Azure Monitor Network", - "path": "public/app/plugins/datasource/azuremonitor/img/azure_monitor_network.png" + "path": "/public/app/plugins/datasource/azuremonitor/img/azure_monitor_network.png" }, { "name": "Azure Monitor CPU", - "path": "public/app/plugins/datasource/azuremonitor/img/azure_monitor_cpu.png" + "path": "/public/app/plugins/datasource/azuremonitor/img/azure_monitor_cpu.png" } ], "version": "1.0.0", @@ -188,8 +188,8 @@ "description": "Categorical charts with group support", "links": null, "logos": { - "small": "public/app/plugins/panel/barchart/img/barchart.svg", - "large": "public/app/plugins/panel/barchart/img/barchart.svg" + "small": "/public/app/plugins/panel/barchart/img/barchart.svg", + "large": "/public/app/plugins/panel/barchart/img/barchart.svg" }, "build": {}, "screenshots": null, @@ -225,8 +225,8 @@ "description": "Horizontal and vertical gauges", "links": null, "logos": { - "small": "public/app/plugins/panel/bargauge/img/icon_bar_gauge.svg", - "large": "public/app/plugins/panel/bargauge/img/icon_bar_gauge.svg" + "small": "/public/app/plugins/panel/bargauge/img/icon_bar_gauge.svg", + "large": "/public/app/plugins/panel/bargauge/img/icon_bar_gauge.svg" }, "build": {}, "screenshots": null, @@ -262,8 +262,8 @@ "description": "", "links": null, "logos": { - "small": "public/app/plugins/panel/candlestick/img/candlestick.svg", - "large": "public/app/plugins/panel/candlestick/img/candlestick.svg" + "small": "/public/app/plugins/panel/candlestick/img/candlestick.svg", + "large": "/public/app/plugins/panel/candlestick/img/candlestick.svg" }, "build": {}, "screenshots": null, @@ -299,8 +299,8 @@ "description": "Explicit element placement", "links": null, "logos": { - "small": "public/app/plugins/panel/canvas/img/icn-canvas.svg", - "large": "public/app/plugins/panel/canvas/img/icn-canvas.svg" + "small": "/public/app/plugins/panel/canvas/img/icn-canvas.svg", + "large": "/public/app/plugins/panel/canvas/img/icn-canvas.svg" }, "build": {}, "screenshots": null, @@ -336,8 +336,8 @@ "description": "Data source for Amazon AWS monitoring service", "links": null, "logos": { - "small": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", - "large": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png" + "small": "/public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", + "large": "/public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png" }, "build": {}, "screenshots": null, @@ -373,8 +373,8 @@ "description": "List of dynamic links to other dashboards", "links": null, "logos": { - "small": "public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg", - "large": "public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg" + "small": "/public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg", + "large": "/public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg" }, "build": {}, "screenshots": null, @@ -410,8 +410,8 @@ "description": "", "links": null, "logos": { - "small": "public/app/plugins/panel/datagrid/img/icn-table-panel.svg", - "large": "public/app/plugins/panel/datagrid/img/icn-table-panel.svg" + "small": "/public/app/plugins/panel/datagrid/img/icn-table-panel.svg", + "large": "/public/app/plugins/panel/datagrid/img/icn-table-panel.svg" }, "build": {}, "screenshots": null, @@ -452,8 +452,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg", - "large": "public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg" + "small": "/public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg", + "large": "/public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg" }, "build": {}, "screenshots": null, @@ -489,8 +489,8 @@ "description": "", "links": null, "logos": { - "small": "public/app/plugins/panel/flamegraph/img/icn-flamegraph.svg", - "large": "public/app/plugins/panel/flamegraph/img/icn-flamegraph.svg" + "small": "/public/app/plugins/panel/flamegraph/img/icn-flamegraph.svg", + "large": "/public/app/plugins/panel/flamegraph/img/icn-flamegraph.svg" }, "build": {}, "screenshots": null, @@ -526,8 +526,8 @@ "description": "Standard gauge visualization", "links": null, "logos": { - "small": "public/app/plugins/panel/gauge/img/icon_gauge.svg", - "large": "public/app/plugins/panel/gauge/img/icon_gauge.svg" + "small": "/public/app/plugins/panel/gauge/img/icon_gauge.svg", + "large": "/public/app/plugins/panel/gauge/img/icon_gauge.svg" }, "build": {}, "screenshots": null, @@ -563,8 +563,8 @@ "description": "Geomap panel", "links": null, "logos": { - "small": "public/app/plugins/panel/geomap/img/icn-geomap.svg", - "large": "public/app/plugins/panel/geomap/img/icn-geomap.svg" + "small": "/public/app/plugins/panel/geomap/img/icn-geomap.svg", + "large": "/public/app/plugins/panel/geomap/img/icn-geomap.svg" }, "build": {}, "screenshots": null, @@ -600,8 +600,8 @@ "description": "", "links": null, "logos": { - "small": "public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg", - "large": "public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg" + "small": "/public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg", + "large": "/public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg" }, "build": {}, "screenshots": null, @@ -637,8 +637,8 @@ "description": "Data source for Google's monitoring service (formerly named Stackdriver)", "links": null, "logos": { - "small": "public/app/plugins/datasource/cloud-monitoring/img/cloud_monitoring_logo.svg", - "large": "public/app/plugins/datasource/cloud-monitoring/img/cloud_monitoring_logo.svg" + "small": "/public/app/plugins/datasource/cloud-monitoring/img/cloud_monitoring_logo.svg", + "large": "/public/app/plugins/datasource/cloud-monitoring/img/cloud_monitoring_logo.svg" }, "build": {}, "screenshots": null, @@ -679,8 +679,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/grafana-pyroscope-datasource/img/grafana_pyroscope_icon.svg", - "large": "public/app/plugins/datasource/grafana-pyroscope-datasource/img/grafana_pyroscope_icon.svg" + "small": "/public/app/plugins/datasource/grafana-pyroscope-datasource/img/grafana_pyroscope_icon.svg", + "large": "/public/app/plugins/datasource/grafana-pyroscope-datasource/img/grafana_pyroscope_icon.svg" }, "build": {}, "screenshots": null, @@ -716,8 +716,8 @@ "description": "The old default graph panel", "links": null, "logos": { - "small": "public/app/plugins/panel/graph/img/icn-graph-panel.svg", - "large": "public/app/plugins/panel/graph/img/icn-graph-panel.svg" + "small": "/public/app/plugins/panel/graph/img/icn-graph-panel.svg", + "large": "/public/app/plugins/panel/graph/img/icn-graph-panel.svg" }, "build": {}, "screenshots": null, @@ -762,8 +762,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/graphite/img/graphite_logo.png", - "large": "public/app/plugins/datasource/graphite/img/graphite_logo.png" + "small": "/public/app/plugins/datasource/graphite/img/graphite_logo.png", + "large": "/public/app/plugins/datasource/graphite/img/graphite_logo.png" }, "build": {}, "screenshots": null, @@ -799,8 +799,8 @@ "description": "Like a histogram over time", "links": null, "logos": { - "small": "public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg", - "large": "public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg" + "small": "/public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg", + "large": "/public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg" }, "build": {}, "screenshots": null, @@ -836,8 +836,8 @@ "description": "", "links": null, "logos": { - "small": "public/app/plugins/panel/histogram/img/histogram.svg", - "large": "public/app/plugins/panel/histogram/img/histogram.svg" + "small": "/public/app/plugins/panel/histogram/img/histogram.svg", + "large": "/public/app/plugins/panel/histogram/img/histogram.svg" }, "build": {}, "screenshots": null, @@ -873,8 +873,8 @@ "description": "Open source time series database", "links": null, "logos": { - "small": "public/app/plugins/datasource/influxdb/img/influxdb_logo.svg", - "large": "public/app/plugins/datasource/influxdb/img/influxdb_logo.svg" + "small": "/public/app/plugins/datasource/influxdb/img/influxdb_logo.svg", + "large": "/public/app/plugins/datasource/influxdb/img/influxdb_logo.svg" }, "build": {}, "screenshots": null, @@ -919,8 +919,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/jaeger/img/jaeger_logo.svg", - "large": "public/app/plugins/datasource/jaeger/img/jaeger_logo.svg" + "small": "/public/app/plugins/datasource/jaeger/img/jaeger_logo.svg", + "large": "/public/app/plugins/datasource/jaeger/img/jaeger_logo.svg" }, "build": {}, "screenshots": null, @@ -956,8 +956,8 @@ "description": "", "links": null, "logos": { - "small": "public/app/plugins/panel/logs/img/icn-logs-panel.svg", - "large": "public/app/plugins/panel/logs/img/icn-logs-panel.svg" + "small": "/public/app/plugins/panel/logs/img/icn-logs-panel.svg", + "large": "/public/app/plugins/panel/logs/img/icn-logs-panel.svg" }, "build": {}, "screenshots": null, @@ -1002,8 +1002,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/loki/img/loki_icon.svg", - "large": "public/app/plugins/datasource/loki/img/loki_icon.svg" + "small": "/public/app/plugins/datasource/loki/img/loki_icon.svg", + "large": "/public/app/plugins/datasource/loki/img/loki_icon.svg" }, "build": {}, "screenshots": null, @@ -1039,8 +1039,8 @@ "description": "Data source for Microsoft SQL Server compatible databases", "links": null, "logos": { - "small": "public/app/plugins/datasource/mssql/img/sql_server_logo.svg", - "large": "public/app/plugins/datasource/mssql/img/sql_server_logo.svg" + "small": "/public/app/plugins/datasource/mssql/img/sql_server_logo.svg", + "large": "/public/app/plugins/datasource/mssql/img/sql_server_logo.svg" }, "build": {}, "screenshots": null, @@ -1076,8 +1076,8 @@ "description": "Data source for MySQL databases", "links": null, "logos": { - "small": "public/app/plugins/datasource/mysql/img/mysql_logo.svg", - "large": "public/app/plugins/datasource/mysql/img/mysql_logo.svg" + "small": "/public/app/plugins/datasource/mysql/img/mysql_logo.svg", + "large": "/public/app/plugins/datasource/mysql/img/mysql_logo.svg" }, "build": {}, "screenshots": null, @@ -1113,8 +1113,8 @@ "description": "RSS feed reader", "links": null, "logos": { - "small": "public/app/plugins/panel/news/img/news.svg", - "large": "public/app/plugins/panel/news/img/news.svg" + "small": "/public/app/plugins/panel/news/img/news.svg", + "large": "/public/app/plugins/panel/news/img/news.svg" }, "build": {}, "screenshots": null, @@ -1150,8 +1150,8 @@ "description": "", "links": null, "logos": { - "small": "public/app/plugins/panel/nodeGraph/img/icn-node-graph.svg", - "large": "public/app/plugins/panel/nodeGraph/img/icn-node-graph.svg" + "small": "/public/app/plugins/panel/nodeGraph/img/icn-node-graph.svg", + "large": "/public/app/plugins/panel/nodeGraph/img/icn-node-graph.svg" }, "build": {}, "screenshots": null, @@ -1187,8 +1187,8 @@ "description": "Open source time series database", "links": null, "logos": { - "small": "public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png", - "large": "public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png" + "small": "/public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png", + "large": "/public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png" }, "build": {}, "screenshots": null, @@ -1229,8 +1229,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/parca/img/logo-small.svg", - "large": "public/app/plugins/datasource/parca/img/logo-small.svg" + "small": "/public/app/plugins/datasource/parca/img/logo-small.svg", + "large": "/public/app/plugins/datasource/parca/img/logo-small.svg" }, "build": {}, "screenshots": null, @@ -1266,8 +1266,8 @@ "description": "The new core pie chart visualization", "links": null, "logos": { - "small": "public/app/plugins/panel/piechart/img/icon_piechart.svg", - "large": "public/app/plugins/panel/piechart/img/icon_piechart.svg" + "small": "/public/app/plugins/panel/piechart/img/icon_piechart.svg", + "large": "/public/app/plugins/panel/piechart/img/icon_piechart.svg" }, "build": {}, "screenshots": null, @@ -1303,8 +1303,8 @@ "description": "Data source for PostgreSQL and compatible databases", "links": null, "logos": { - "small": "public/app/plugins/datasource/postgres/img/postgresql_logo.svg", - "large": "public/app/plugins/datasource/postgres/img/postgresql_logo.svg" + "small": "/public/app/plugins/datasource/postgres/img/postgresql_logo.svg", + "large": "/public/app/plugins/datasource/postgres/img/postgresql_logo.svg" }, "build": {}, "screenshots": null, @@ -1345,8 +1345,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/prometheus/img/prometheus_logo.svg", - "large": "public/app/plugins/datasource/prometheus/img/prometheus_logo.svg" + "small": "/public/app/plugins/datasource/prometheus/img/prometheus_logo.svg", + "large": "/public/app/plugins/datasource/prometheus/img/prometheus_logo.svg" }, "build": {}, "screenshots": null, @@ -1382,8 +1382,8 @@ "description": "Big stat values \u0026 sparklines", "links": null, "logos": { - "small": "public/app/plugins/panel/stat/img/icn-singlestat-panel.svg", - "large": "public/app/plugins/panel/stat/img/icn-singlestat-panel.svg" + "small": "/public/app/plugins/panel/stat/img/icn-singlestat-panel.svg", + "large": "/public/app/plugins/panel/stat/img/icn-singlestat-panel.svg" }, "build": {}, "screenshots": null, @@ -1419,8 +1419,8 @@ "description": "State changes and durations", "links": null, "logos": { - "small": "public/app/plugins/panel/state-timeline/img/timeline.svg", - "large": "public/app/plugins/panel/state-timeline/img/timeline.svg" + "small": "/public/app/plugins/panel/state-timeline/img/timeline.svg", + "large": "/public/app/plugins/panel/state-timeline/img/timeline.svg" }, "build": {}, "screenshots": null, @@ -1456,8 +1456,8 @@ "description": "Periodic status history", "links": null, "logos": { - "small": "public/app/plugins/panel/status-history/img/status.svg", - "large": "public/app/plugins/panel/status-history/img/status.svg" + "small": "/public/app/plugins/panel/status-history/img/status.svg", + "large": "/public/app/plugins/panel/status-history/img/status.svg" }, "build": {}, "screenshots": null, @@ -1493,8 +1493,8 @@ "description": "Supports many column styles", "links": null, "logos": { - "small": "public/app/plugins/panel/table/img/icn-table-panel.svg", - "large": "public/app/plugins/panel/table/img/icn-table-panel.svg" + "small": "/public/app/plugins/panel/table/img/icn-table-panel.svg", + "large": "/public/app/plugins/panel/table/img/icn-table-panel.svg" }, "build": {}, "screenshots": null, @@ -1530,8 +1530,8 @@ "description": "Table Panel for Grafana", "links": null, "logos": { - "small": "public/app/plugins/panel/table-old/img/icn-table-panel.svg", - "large": "public/app/plugins/panel/table-old/img/icn-table-panel.svg" + "small": "/public/app/plugins/panel/table-old/img/icn-table-panel.svg", + "large": "/public/app/plugins/panel/table-old/img/icn-table-panel.svg" }, "build": {}, "screenshots": null, @@ -1572,8 +1572,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/tempo/img/tempo_logo.svg", - "large": "public/app/plugins/datasource/tempo/img/tempo_logo.svg" + "small": "/public/app/plugins/datasource/tempo/img/tempo_logo.svg", + "large": "/public/app/plugins/datasource/tempo/img/tempo_logo.svg" }, "build": {}, "screenshots": null, @@ -1609,8 +1609,8 @@ "description": "Generates test data in different forms", "links": null, "logos": { - "small": "public/app/plugins/datasource/testdata/img/testdata.svg", - "large": "public/app/plugins/datasource/testdata/img/testdata.svg" + "small": "/public/app/plugins/datasource/testdata/img/testdata.svg", + "large": "/public/app/plugins/datasource/testdata/img/testdata.svg" }, "build": {}, "screenshots": null, @@ -1646,8 +1646,8 @@ "description": "Supports markdown and html content", "links": null, "logos": { - "small": "public/app/plugins/panel/text/img/icn-text-panel.svg", - "large": "public/app/plugins/panel/text/img/icn-text-panel.svg" + "small": "/public/app/plugins/panel/text/img/icn-text-panel.svg", + "large": "/public/app/plugins/panel/text/img/icn-text-panel.svg" }, "build": {}, "screenshots": null, @@ -1683,8 +1683,8 @@ "description": "Time based line, area and bar charts", "links": null, "logos": { - "small": "public/app/plugins/panel/timeseries/img/icn-timeseries-panel.svg", - "large": "public/app/plugins/panel/timeseries/img/icn-timeseries-panel.svg" + "small": "/public/app/plugins/panel/timeseries/img/icn-timeseries-panel.svg", + "large": "/public/app/plugins/panel/timeseries/img/icn-timeseries-panel.svg" }, "build": {}, "screenshots": null, @@ -1720,8 +1720,8 @@ "description": "", "links": null, "logos": { - "small": "public/app/plugins/panel/traces/img/traces-panel.svg", - "large": "public/app/plugins/panel/traces/img/traces-panel.svg" + "small": "/public/app/plugins/panel/traces/img/traces-panel.svg", + "large": "/public/app/plugins/panel/traces/img/traces-panel.svg" }, "build": {}, "screenshots": null, @@ -1757,8 +1757,8 @@ "description": "Like timeseries, but when x != time", "links": null, "logos": { - "small": "public/app/plugins/panel/trend/img/trend.svg", - "large": "public/app/plugins/panel/trend/img/trend.svg" + "small": "/public/app/plugins/panel/trend/img/trend.svg", + "large": "/public/app/plugins/panel/trend/img/trend.svg" }, "build": {}, "screenshots": null, @@ -1794,8 +1794,8 @@ "description": "", "links": null, "logos": { - "small": "public/app/plugins/panel/welcome/img/icn-dashlist-panel.svg", - "large": "public/app/plugins/panel/welcome/img/icn-dashlist-panel.svg" + "small": "/public/app/plugins/panel/welcome/img/icn-dashlist-panel.svg", + "large": "/public/app/plugins/panel/welcome/img/icn-dashlist-panel.svg" }, "build": {}, "screenshots": null, @@ -1831,8 +1831,8 @@ "description": "", "links": null, "logos": { - "small": "public/app/plugins/panel/xychart/img/icn-xychart.svg", - "large": "public/app/plugins/panel/xychart/img/icn-xychart.svg" + "small": "/public/app/plugins/panel/xychart/img/icn-xychart.svg", + "large": "/public/app/plugins/panel/xychart/img/icn-xychart.svg" }, "build": {}, "screenshots": null, @@ -1873,8 +1873,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/zipkin/img/zipkin-logo.svg", - "large": "public/app/plugins/datasource/zipkin/img/zipkin-logo.svg" + "small": "/public/app/plugins/datasource/zipkin/img/zipkin-logo.svg", + "large": "/public/app/plugins/datasource/zipkin/img/zipkin-logo.svg" }, "build": {}, "screenshots": null, diff --git a/public/app/angular/AngularApp.ts b/public/app/angular/AngularApp.ts index 2d64500d54b..763ce7b1195 100644 --- a/public/app/angular/AngularApp.ts +++ b/public/app/angular/AngularApp.ts @@ -7,14 +7,14 @@ import 'vendor/bootstrap/bootstrap'; import angular from 'angular'; // eslint-disable-line no-duplicate-imports import { extend } from 'lodash'; -import { getTemplateSrv } from '@grafana/runtime'; -import coreModule, { angularModules } from 'app/angular/core_module'; +import { getTemplateSrv, SystemJS } from '@grafana/runtime'; +import { coreModule, angularModules } from 'app/angular/core_module'; import appEvents from 'app/core/app_events'; import { config } from 'app/core/config'; import { contextSrv } from 'app/core/services/context_srv'; import { DashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; -import { exposeToPlugin } from 'app/features/plugins/plugin_loader'; +import { buildImportMap } from 'app/features/plugins/loader/utils'; import * as sdk from 'app/plugins/sdk'; import { registerAngularDirectives } from './angular_wrappers'; @@ -23,6 +23,22 @@ import { monkeyPatchInjectorWithPreAssignedBindings } from './injectorMonkeyPatc import { promiseToDigest } from './promiseToDigest'; import { registerComponents } from './registerComponents'; +// Angular plugin dependencies map +const importMap = { + angular: angular, + 'app/core/core_module': { + default: coreModule, + __useDefault: true, + }, + 'app/core/core': { + appEvents: appEvents, + contextSrv: contextSrv, + coreModule: coreModule, + }, + 'app/plugins/sdk': sdk, + 'app/core/utils/promiseToDigest': { promiseToDigest }, +} as Record; + export class AngularApp { ngModuleDependencies: any[]; preBootModules: any[]; @@ -105,17 +121,9 @@ 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, - }); + const imports = buildImportMap(importMap); + // pass the map of module names so systemjs can resolve them + SystemJS.addImportMap({ imports }); // disable tool tip animation $.fn.tooltip.defaults.animation = false; diff --git a/public/app/angular/components/plugin_component.test.ts b/public/app/angular/components/plugin_component.test.ts deleted file mode 100644 index e5205751fc6..00000000000 --- a/public/app/angular/components/plugin_component.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { config } from '@grafana/runtime'; - -import { relativeTemplateUrlToCDN } from './plugin_component'; - -describe('Plugin Component', () => { - describe('relativeTemplateUrlToCDN()', () => { - it('should create a proper path', () => { - config.pluginsCDNBaseURL = 'http://my-host.com'; - - const templateUrl = 'partials/module.html'; - const baseUrl = 'plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel'; - const expectedUrl = - 'http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/partials/module.html'; - - expect(relativeTemplateUrlToCDN(templateUrl, baseUrl)).toBe(expectedUrl); - }); - }); -}); diff --git a/public/app/angular/components/plugin_component.ts b/public/app/angular/components/plugin_component.ts index a22b122c31c..3eccc7e0d18 100644 --- a/public/app/angular/components/plugin_component.ts +++ b/public/app/angular/components/plugin_component.ts @@ -8,20 +8,6 @@ import config from 'app/core/config'; import { importPanelPlugin } from '../../features/plugins/importPanelPlugin'; import { importDataSourcePlugin, importAppPlugin } from '../../features/plugins/plugin_loader'; -export function relativeTemplateUrlToCDN(templateUrl: string, baseUrl: string) { - if (!templateUrl) { - return undefined; - } - - // the templateUrl may have already been updated with the hostname - if (templateUrl.startsWith(config.pluginsCDNBaseURL)) { - return templateUrl; - } - - // use the 'plugin-cdn' key to load via cdn - return `${baseUrl.replace('plugin-cdn/', `${config.pluginsCDNBaseURL}/`)}/${templateUrl}`; -} - coreModule.directive('pluginComponent', ['$compile', '$http', '$templateCache', '$location', pluginDirectiveLoader]); function pluginDirectiveLoader($compile: any, $http: any, $templateCache: any, $location: ILocationService) { @@ -50,12 +36,8 @@ function pluginDirectiveLoader($compile: any, $http: any, $templateCache: any, $ } function getPluginComponentDirective(options: any) { - if (options.baseUrl.includes('plugin-cdn')) { - options.Component.templateUrl = relativeTemplateUrlToCDN(options.Component.templateUrl, options.baseUrl); - } else { - // handle relative template urls for plugin templates - options.Component.templateUrl = relativeTemplateUrlToAbs(options.Component.templateUrl, options.baseUrl); - } + // handle relative template urls for plugin templates + options.Component.templateUrl = relativeTemplateUrlToAbs(options.Component.templateUrl, options.baseUrl); return () => { return { @@ -105,11 +87,7 @@ function pluginDirectiveLoader($compile: any, $http: any, $templateCache: any, $ } if (panelInfo) { - if (panelInfo.baseUrl.includes('plugin-cdn')) { - PanelCtrl.templateUrl = relativeTemplateUrlToCDN(PanelCtrl.templateUrl, panelInfo.baseUrl); - } else { - PanelCtrl.templateUrl = relativeTemplateUrlToAbs(PanelCtrl.templateUrl, panelInfo.baseUrl); - } + PanelCtrl.templateUrl = relativeTemplateUrlToAbs(PanelCtrl.templateUrl, panelInfo.baseUrl); } PanelCtrl.templatePromise = getTemplate(PanelCtrl).then((template: any) => { diff --git a/public/app/features/panel/state/getAllSuggestions.test.ts b/public/app/features/panel/state/getAllSuggestions.test.ts index 590575b01b4..17b236ed1ec 100644 --- a/public/app/features/panel/state/getAllSuggestions.test.ts +++ b/public/app/features/panel/state/getAllSuggestions.test.ts @@ -18,7 +18,7 @@ jest.unmock('app/features/plugins/plugin_loader'); for (const pluginId of panelsToCheckFirst) { config.panels[pluginId] = { - module: `app/plugins/panel/${pluginId}/module`, + module: `core:plugin/${pluginId}`, } as PanelPluginMeta; } diff --git a/public/app/features/plugins/admin/state/actions.ts b/public/app/features/plugins/admin/state/actions.ts index fb5cac89dbc..ee8d61fe361 100644 --- a/public/app/features/plugins/admin/state/actions.ts +++ b/public/app/features/plugins/admin/state/actions.ts @@ -5,7 +5,7 @@ import { getBackendSrv, isFetchError } from '@grafana/runtime'; import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin'; import { StoreState, ThunkResult } from 'app/types'; -import { invalidatePluginInCache } from '../../systemjsPlugins/pluginCacheBuster'; +import { invalidatePluginInCache } from '../../loader/cache'; import { getRemotePlugins, getPluginErrors, diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index 5ffa8176911..66053733ba0 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -77,67 +77,67 @@ const canvasPanel = async () => await import(/* webpackChunkName: "canvasPanel" const graphPanel = async () => await import(/* webpackChunkName: "graphPlugin" */ 'app/plugins/panel/graph/module'); const heatmapPanel = async () => await import(/* webpackChunkName: "heatmapPanel" */ '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, - 'app/plugins/datasource/cloudwatch/module': cloudwatchPlugin, - 'app/plugins/datasource/dashboard/module': dashboardDSPlugin, - 'app/plugins/datasource/elasticsearch/module': elasticsearchPlugin, - 'app/plugins/datasource/opentsdb/module': opentsdbPlugin, - 'app/plugins/datasource/grafana/module': grafanaPlugin, - 'app/plugins/datasource/influxdb/module': influxdbPlugin, - 'app/plugins/datasource/loki/module': lokiPlugin, - 'app/plugins/datasource/jaeger/module': jaegerPlugin, - 'app/plugins/datasource/zipkin/module': zipkinPlugin, - 'app/plugins/datasource/mixed/module': mixedPlugin, - 'app/plugins/datasource/mysql/module': mysqlPlugin, - 'app/plugins/datasource/postgres/module': postgresPlugin, - 'app/plugins/datasource/mssql/module': mssqlPlugin, - 'app/plugins/datasource/prometheus/module': prometheusPlugin, - 'app/plugins/datasource/testdata/module': testDataDSPlugin, - 'app/plugins/datasource/cloud-monitoring/module': cloudMonitoringPlugin, - 'app/plugins/datasource/azuremonitor/module': azureMonitorPlugin, - 'app/plugins/datasource/tempo/module': tempoPlugin, - 'app/plugins/datasource/alertmanager/module': alertmanagerPlugin, - 'app/plugins/datasource/grafana-pyroscope-datasource/module': pyroscopePlugin, - 'app/plugins/datasource/parca/module': parcaPlugin, - - 'app/plugins/panel/text/module': textPanel, - 'app/plugins/panel/timeseries/module': timeseriesPanel, - 'app/plugins/panel/trend/module': trendPanel, - 'app/plugins/panel/state-timeline/module': stateTimelinePanel, - 'app/plugins/panel/status-history/module': statusHistoryPanel, - 'app/plugins/panel/candlestick/module': candlestickPanel, - 'app/plugins/panel/graph/module': graphPanel, - 'app/plugins/panel/xychart/module': xyChartPanel, - 'app/plugins/panel/geomap/module': geomapPanel, - 'app/plugins/panel/canvas/module': canvasPanel, - 'app/plugins/panel/dashlist/module': dashListPanel, - 'app/plugins/panel/alertlist/module': alertListPanel, - 'app/plugins/panel/annolist/module': annoListPanel, - 'app/plugins/panel/heatmap/module': heatmapPanel, - 'app/plugins/panel/table/module': tablePanel, - '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, - 'app/plugins/panel/datagrid/module': dataGridPanel, - 'app/plugins/panel/debug/module': debugPanel, - 'app/plugins/panel/flamegraph/module': flamegraphPanel, - 'app/plugins/panel/gettingstarted/module': gettingStartedPanel, - 'app/plugins/panel/gauge/module': gaugePanel, - 'app/plugins/panel/piechart/module': pieChartPanel, - 'app/plugins/panel/bargauge/module': barGaugePanel, - 'app/plugins/panel/barchart/module': barChartPanel, - 'app/plugins/panel/logs/module': logsPanel, - 'app/plugins/panel/traces/module': tracesPanel, - 'app/plugins/panel/welcome/module': welcomeBanner, - 'app/plugins/panel/nodeGraph/module': nodeGraph, - 'app/plugins/panel/histogram/module': histogramPanel, - 'app/plugins/panel/alertGroups/module': alertGroupsPanel, +const builtInPlugins: Record Promise)> = { + // datasources + 'core:plugin/graphite': graphitePlugin, + 'core:plugin/cloudwatch': cloudwatchPlugin, + 'core:plugin/dashboard': dashboardDSPlugin, + 'core:plugin/elasticsearch': elasticsearchPlugin, + 'core:plugin/opentsdb': opentsdbPlugin, + 'core:plugin/grafana': grafanaPlugin, + 'core:plugin/influxdb': influxdbPlugin, + 'core:plugin/loki': lokiPlugin, + 'core:plugin/jaeger': jaegerPlugin, + 'core:plugin/zipkin': zipkinPlugin, + 'core:plugin/mixed': mixedPlugin, + 'core:plugin/mysql': mysqlPlugin, + 'core:plugin/postgres': postgresPlugin, + 'core:plugin/mssql': mssqlPlugin, + 'core:plugin/prometheus': prometheusPlugin, + 'core:plugin/testdata': testDataDSPlugin, + 'core:plugin/cloud-monitoring': cloudMonitoringPlugin, + 'core:plugin/azuremonitor': azureMonitorPlugin, + 'core:plugin/tempo': tempoPlugin, + 'core:plugin/alertmanager': alertmanagerPlugin, + 'core:plugin/grafana-pyroscope-datasource': pyroscopePlugin, + 'core:plugin/parca': parcaPlugin, + // panels + 'core:plugin/text': textPanel, + 'core:plugin/timeseries': timeseriesPanel, + 'core:plugin/trend': trendPanel, + 'core:plugin/state-timeline': stateTimelinePanel, + 'core:plugin/status-history': statusHistoryPanel, + 'core:plugin/candlestick': candlestickPanel, + 'core:plugin/graph': graphPanel, + 'core:plugin/xychart': xyChartPanel, + 'core:plugin/geomap': geomapPanel, + 'core:plugin/canvas': canvasPanel, + 'core:plugin/dashlist': dashListPanel, + 'core:plugin/alertlist': alertListPanel, + 'core:plugin/annolist': annoListPanel, + 'core:plugin/heatmap': heatmapPanel, + 'core:plugin/table': tablePanel, + 'core:plugin/table-old': tableOldPanel, + 'core:plugin/news': newsPanel, + 'core:plugin/live': livePanel, + 'core:plugin/stat': statPanel, + 'core:plugin/datagrid': dataGridPanel, + 'core:plugin/debug': debugPanel, + 'core:plugin/flamegraph': flamegraphPanel, + 'core:plugin/gettingstarted': gettingStartedPanel, + 'core:plugin/gauge': gaugePanel, + 'core:plugin/piechart': pieChartPanel, + 'core:plugin/bargauge': barGaugePanel, + 'core:plugin/barchart': barChartPanel, + 'core:plugin/logs': logsPanel, + 'core:plugin/traces': tracesPanel, + 'core:plugin/welcome': welcomeBanner, + 'core:plugin/nodeGraph': nodeGraph, + 'core:plugin/histogram': histogramPanel, + 'core:plugin/alertGroups': alertGroupsPanel, }; export default builtInPlugins; diff --git a/public/app/features/plugins/cdn/utils.test.ts b/public/app/features/plugins/cdn/utils.test.ts index b2fda523950..c9325ebb9a1 100644 --- a/public/app/features/plugins/cdn/utils.test.ts +++ b/public/app/features/plugins/cdn/utils.test.ts @@ -1,19 +1,12 @@ -import { config } from '@grafana/runtime'; - -import { extractPluginIdVersionFromUrl, transformPluginSourceForCDN } from './utils'; +import { transformPluginSourceForCDN } from './utils'; describe('Plugin CDN Utils', () => { - describe('transformPluginSourceForCdn', () => { - // const localUrl = - // 'http://localhost:3000/public/plugin-cdn/grafana-worldmap-panel/0.3.3/grafana-worldmap-panel/module.js'; - const pluginId = 'grafana-worldmap-panel'; - const version = '0.3.3'; - config.pluginsCDNBaseURL = 'http://my-host.com'; + const url = 'http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module.js'; + describe('transformPluginSourceForCdn', () => { it('should update the default local path to use the CDN path', () => { const translatedLoad = transformPluginSourceForCDN({ - pluginId, - version, + url, source: 'public/plugins/grafana-worldmap-panel/template.html', }); expect(translatedLoad).toBe( @@ -30,7 +23,7 @@ describe('Plugin CDN Utils', () => { const a = "http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/template.html"; const img = ""; `; - const translatedLoad = transformPluginSourceForCDN({ pluginId, version, source }); + const translatedLoad = transformPluginSourceForCDN({ url, source }); expect(translatedLoad).toBe(expectedSource); }); @@ -43,7 +36,7 @@ describe('Plugin CDN Utils', () => { const a = "http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/template.html"; const img = ""; `; - const translatedLoad = transformPluginSourceForCDN({ pluginId, version, source }); + const translatedLoad = transformPluginSourceForCDN({ url, source }); expect(translatedLoad).toBe(expectedSource); }); @@ -62,12 +55,12 @@ describe('Plugin CDN Utils', () => { ".json" ) `; - const translatedLoad = transformPluginSourceForCDN({ pluginId, version, source }); + const translatedLoad = transformPluginSourceForCDN({ url, source }); expect(translatedLoad).toBe(expectedSource); }); - it('should replace sourcemap locations', () => { + it('should only replace sourcemap locations if transformSourceMapUrl is true', () => { const source = ` Zn(t,e)},t.Rectangle=ui,t.rectangle=function(t,e){return new ui(t,e)},t.Map=He,t.map=function(t,e){return new He(t,e)}}(e)}])}); //# sourceMappingURL=module.js.map @@ -76,29 +69,21 @@ describe('Plugin CDN Utils', () => { Zn(t,e)},t.Rectangle=ui,t.rectangle=function(t,e){return new ui(t,e)},t.Map=He,t.map=function(t,e){return new He(t,e)}}(e)}])}); //# sourceMappingURL=http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module.js.map `; - const translatedLoad = transformPluginSourceForCDN({ pluginId, version, source }); + const translatedLoad = transformPluginSourceForCDN({ url, source }); - expect(translatedLoad).toBe(expectedSource); + expect(translatedLoad).toBe(source); + + const translatedLoadWithSourceMapUrl = transformPluginSourceForCDN({ url, source, transformSourceMapURL: true }); + + expect(translatedLoadWithSourceMapUrl).toBe(expectedSource); }); it('should replace css paths', () => { const source = `(0,o.loadPluginCss)({dark:"plugins/grafana-worldmap-panel/css/worldmap.dark.css",light:"plugins/grafana-worldmap-panel/css/worldmap.light.css"}),`; const expectedSource = `(0,o.loadPluginCss)({dark:"http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/css/worldmap.dark.css",light:"http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/css/worldmap.light.css"}),`; - const translatedLoad = transformPluginSourceForCDN({ pluginId, version, source }); + const translatedLoad = transformPluginSourceForCDN({ url, source }); expect(translatedLoad).toBe(expectedSource); }); }); - describe('extractPluginIdVersionFromUrl', () => { - it('should extract the plugin id and version from a path', () => { - const source = - 'http://localhost:3000/public/plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module.js'; - const expected = { - id: 'grafana-worldmap-panel', - version: '0.3.3', - }; - const expectedExtractedPluginDeets = extractPluginIdVersionFromUrl(source); - expect(expectedExtractedPluginDeets).toEqual(expected); - }); - }); }); diff --git a/public/app/features/plugins/cdn/utils.ts b/public/app/features/plugins/cdn/utils.ts index 04e8b7d4b6d..4184bcd1992 100644 --- a/public/app/features/plugins/cdn/utils.ts +++ b/public/app/features/plugins/cdn/utils.ts @@ -1,45 +1,32 @@ -import { config } from '@grafana/runtime'; - -import { PLUGIN_CDN_URL_KEY } from '../constants'; /* - Given an "expected" address of `http://localhost/public/plugin-cdn/{pluginId}/{version}/public/plugins/{pluginId}` - this function will return the plugin id and version. - */ -export function extractPluginIdVersionFromUrl(address: string) { - const path = new URL(address).pathname; - const match = path.split('/'); - return { id: match[3], version: match[4] }; -} - -/* - Transforms plugin's source for CDNs loa. + Transforms CDN hosted plugin source code. Plugins that require loading via a CDN need to have their asset paths translated to point to the configured CDN. e.g. public/plugins/my-plugin/data/ -> http://my-host.com/my-plugin/0.3.3/public/plugins/my-plugin/data/ */ export function transformPluginSourceForCDN({ - pluginId, - version, + url, source, + transformSourceMapURL = false, }: { - pluginId: string; - version: string; + url: string; source: string; + transformSourceMapURL?: boolean; }): string { - const baseAddress = `${config.pluginsCDNBaseURL}/${pluginId}/${version}`; + const splitUrl = url.split('/public/plugins/'); + const baseAddress = splitUrl[0]; + const pluginId = splitUrl[1].split('/')[0]; + // handle basic asset paths that include public/plugins let newSource = source; - newSource = source.replace(/(\/?)(public\/plugins)/g, `${baseAddress}/$2`); - // handle custom plugin css (light and dark themes) + newSource = newSource.replace(/(\/?)(public\/plugins)/g, `${baseAddress}/$2`); newSource = newSource.replace(/(["|'])(plugins\/.+?.css)(["|'])/g, `$1${baseAddress}/public/$2$3`); - // handle external sourcemap links - newSource = newSource.replace( - /(\/\/#\ssourceMappingURL=)(.+)\.map/g, - `$1${baseAddress}/public/plugins/${pluginId}/$2.map` - ); + + if (transformSourceMapURL) { + newSource = newSource.replace( + /(\/\/#\ssourceMappingURL=)(.+)\.map/g, + `$1${baseAddress}/public/plugins/${pluginId}/$2.map` + ); + } + return newSource; } - -export function getPluginCdnResourceUrl(localPath: string): string { - const pluginPath = localPath.split(`/public/${PLUGIN_CDN_URL_KEY}/`); - return `${config.pluginsCDNBaseURL}/${pluginPath[1]}`; -} diff --git a/public/app/features/plugins/constants.ts b/public/app/features/plugins/constants.ts deleted file mode 100644 index 5a49d1e3845..00000000000 --- a/public/app/features/plugins/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const PLUGIN_CDN_URL_KEY = 'plugin-cdn'; diff --git a/public/app/features/plugins/systemjsPlugins/pluginCacheBuster.test.ts b/public/app/features/plugins/loader/cache.test.ts similarity index 71% rename from public/app/features/plugins/systemjsPlugins/pluginCacheBuster.test.ts rename to public/app/features/plugins/loader/cache.test.ts index c8d046bcaa6..b0253b75663 100644 --- a/public/app/features/plugins/systemjsPlugins/pluginCacheBuster.test.ts +++ b/public/app/features/plugins/loader/cache.test.ts @@ -1,47 +1,47 @@ import * as pluginSettings from '../pluginSettings'; -import { invalidatePluginInCache, locateWithCache, registerPluginInCache } from './pluginCacheBuster'; +import { invalidatePluginInCache, resolveWithCache, registerPluginInCache } from './cache'; -describe('PluginCacheBuster', () => { +describe('Plugin Cache', () => { const now = 12345; it('should append plugin version as cache flag if plugin is registered in buster', () => { const slug = 'bubble-chart-1'; const version = 'v1.0.0'; - const path = resolvePath(slug); + const path = `/public/plugins/${slug}/module.js`; const address = `http://localhost:3000/public/${path}.js`; registerPluginInCache({ path, version }); const url = `${address}?_cache=${encodeURI(version)}`; - expect(locateWithCache({ address }, now)).toBe(url); + expect(resolveWithCache(address, now)).toBe(url); }); it('should append Date.now as cache flag if plugin is not registered in buster', () => { const slug = 'bubble-chart-2'; - const address = `http://localhost:3000/public/${resolvePath(slug)}.js`; + const address = `http://localhost:3000/public/plugins/${slug}/module.js`; const url = `${address}?_cache=${encodeURI(String(now))}`; - expect(locateWithCache({ address }, now)).toBe(url); + expect(resolveWithCache(address, now)).toBe(url); }); it('should append Date.now as cache flag if plugin is invalidated in buster', () => { const slug = 'bubble-chart-3'; const version = 'v1.0.0'; - const path = resolvePath(slug); + const path = `/public/plugins/${slug}/module.js`; const address = `http://localhost:3000/public/${path}.js`; registerPluginInCache({ path, version }); invalidatePluginInCache(slug); const url = `${address}?_cache=${encodeURI(String(now))}`; - expect(locateWithCache({ address }, now)).toBe(url); + expect(resolveWithCache(address, now)).toBe(url); }); it('should also clear plugin settings cache', () => { const slug = 'bubble-chart-3'; const version = 'v1.0.0'; - const path = resolvePath(slug); + const path = `/public/plugins/${slug}/module.js`; const clearPluginSettingsCacheSpy = jest.spyOn(pluginSettings, 'clearPluginSettingsCache'); @@ -52,7 +52,3 @@ describe('PluginCacheBuster', () => { expect(clearPluginSettingsCacheSpy).toBeCalledWith('bubble-chart-3'); }); }); - -function resolvePath(slug: string): string { - return `plugins/${slug}/module`; -} diff --git a/public/app/features/plugins/systemjsPlugins/pluginCacheBuster.ts b/public/app/features/plugins/loader/cache.ts similarity index 66% rename from public/app/features/plugins/systemjsPlugins/pluginCacheBuster.ts rename to public/app/features/plugins/loader/cache.ts index af18eb43dd8..739c4c58072 100644 --- a/public/app/features/plugins/systemjsPlugins/pluginCacheBuster.ts +++ b/public/app/features/plugins/loader/cache.ts @@ -9,8 +9,9 @@ type CacheablePlugin = { }; export function registerPluginInCache({ path, version }: CacheablePlugin): void { - if (!cache[path]) { - cache[path] = encodeURI(version); + const key = extractPath(path); + if (key && !cache[key]) { + cache[key] = encodeURI(version); } } @@ -22,21 +23,19 @@ export function invalidatePluginInCache(pluginId: string): void { clearPluginSettingsCache(pluginId); } -export function locateWithCache(load: { address: string }, defaultBust = initializedAt): string { - const { address } = load; - const path = extractPath(address); - +export function resolveWithCache(url: string, defaultBust = initializedAt): string { + const path = extractPath(url); if (!path) { - return `${address}?_cache=${defaultBust}`; + return `${url}?_cache=${defaultBust}`; } const version = cache[path]; const bust = version || defaultBust; - return `${address}?_cache=${bust}`; + return `${url}?_cache=${bust}`; } function extractPath(address: string): string | undefined { - const match = /\/public\/(plugins\/.+\/module)\.js/i.exec(address); + const match = /\/.+\/(plugins\/.+\/module)\.js/i.exec(address); if (!match) { return; } diff --git a/public/app/features/plugins/loader/constants.ts b/public/app/features/plugins/loader/constants.ts new file mode 100644 index 00000000000..9c247ae4d07 --- /dev/null +++ b/public/app/features/plugins/loader/constants.ts @@ -0,0 +1,5 @@ +export const SHARED_DEPENDENCY_PREFIX = 'package'; +export const LOAD_PLUGIN_CSS_REGEX = /^plugins.+\.css$/i; +export const JS_CONTENT_TYPE_REGEX = /^(text|application)\/(x-)?javascript(;|$)/; +export const ENDS_WITH_FILE_EXTENSION_REGEX = /\/?\.[a-zA-Z]{2,}$/; +export const IS_SYSTEM_MODULE_REGEX = /System\.register\(/; diff --git a/public/app/features/plugins/loader/pluginLoader.mock.ts b/public/app/features/plugins/loader/pluginLoader.mock.ts new file mode 100644 index 00000000000..c2332bdcc6a --- /dev/null +++ b/public/app/features/plugins/loader/pluginLoader.mock.ts @@ -0,0 +1,37 @@ +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + +export const mockAmdModule = `define([], function() { + return function() { + console.log('AMD module loaded'); + var pluginPath = "/public/plugins/"; + } +});`; + +export const mockSystemModule = `System.register(['./dependencyA'], function (_export, _context) { + "use strict"; + + var DependencyACtrl; + return { + setters: [function (_dependencyA) { + DependencyACtrl = _dependencyA.DependencyACtrl; + }], + execute: function () { + _export('PanelCtrl', DependencyACtrl); + } + }; +});`; + +const server = setupServer( + rest.get('/public/plugins/my-amd-plugin/module.js', async (_req, res, ctx) => + res(ctx.status(200), ctx.set('Content-Type', 'text/javascript'), ctx.body(mockAmdModule)) + ), + rest.get('/public/plugins/my-system-plugin/module.js', async (_req, res, ctx) => + res(ctx.status(200), ctx.set('Content-Type', 'text/javascript'), ctx.body(mockSystemModule)) + ), + rest.get('http://my-cdn.com/plugins/my-plugin/v1.0.0/public/plugins/my-plugin/module.js', async (_req, res, ctx) => + res(ctx.status(200), ctx.set('Content-Type', 'text/javascript'), ctx.body(mockAmdModule)) + ) +); + +export { server }; diff --git a/public/app/features/plugins/loader/sharedDependencies.ts b/public/app/features/plugins/loader/sharedDependencies.ts new file mode 100644 index 00000000000..f1b4d2663b5 --- /dev/null +++ b/public/app/features/plugins/loader/sharedDependencies.ts @@ -0,0 +1,140 @@ +import * as emotion from '@emotion/css'; +import * as emotionReact from '@emotion/react'; +import * as d3 from 'd3'; +import jquery from 'jquery'; +import _ from 'lodash'; // eslint-disable-line lodash/import-scope +import moment from 'moment'; // eslint-disable-line no-restricted-imports +import prismjs from 'prismjs'; +import react from 'react'; +import reactDom from 'react-dom'; +import * as reactRedux from 'react-redux'; // eslint-disable-line no-restricted-imports +import * as reactRouterDom from 'react-router-dom'; +import * as reactRouterCompat from 'react-router-dom-v5-compat'; +import * as redux from 'redux'; +import * as rxjs from 'rxjs'; +import * as rxjsOperators from 'rxjs/operators'; +import slate from 'slate'; +import slatePlain from 'slate-plain-serializer'; +import slateReact from 'slate-react'; + +import 'vendor/flot/jquery.flot'; +import 'vendor/flot/jquery.flot.selection'; +import 'vendor/flot/jquery.flot.time'; +import 'vendor/flot/jquery.flot.stack'; +import 'vendor/flot/jquery.flot.stackpercent'; +import 'vendor/flot/jquery.flot.fillbelow'; +import 'vendor/flot/jquery.flot.crosshair'; +import 'vendor/flot/jquery.flot.dashes'; +import 'vendor/flot/jquery.flot.gauge'; + +import * as grafanaData from '@grafana/data'; +import * as grafanaRuntime from '@grafana/runtime'; +import * as grafanaUIraw from '@grafana/ui'; +import TableModel from 'app/core/TableModel'; +import config from 'app/core/config'; +import { appEvents, contextSrv } from 'app/core/core'; +import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv'; +import impressionSrv from 'app/core/services/impression_srv'; +import TimeSeries from 'app/core/time_series2'; +import * as flatten from 'app/core/utils/flatten'; +import kbn from 'app/core/utils/kbn'; +import * as ticks from 'app/core/utils/ticks'; + +// Help the 6.4 to 6.5 migration +// The base classes were moved from @grafana/ui to @grafana/data +// This exposes the same classes on both import paths +const grafanaUI: Record = grafanaUIraw; +grafanaUI.PanelPlugin = grafanaData.PanelPlugin; +grafanaUI.DataSourcePlugin = grafanaData.DataSourcePlugin; +grafanaUI.AppPlugin = grafanaData.AppPlugin; +grafanaUI.DataSourceApi = grafanaData.DataSourceApi; + +const jQueryFlotDeps = [ + 'jquery.flot.crosshair', + 'jquery.flot.events', + 'jquery.flot.fillbelow', + 'jquery.flot.gauge', + 'jquery.flot.pie', + 'jquery.flot.selection', + 'jquery.flot.stack', + 'jquery.flot.stackpercent', + 'jquery.flot.time', + 'jquery.flot', +].reduce((acc, flotDep) => ({ ...acc, [flotDep]: { fakeDep: 1 } }), {}); + +export const sharedDependenciesMap: Record = { + '@emotion/css': emotion, + '@emotion/react': emotionReact, + '@grafana/data': grafanaData, + '@grafana/runtime': grafanaRuntime, + '@grafana/slate-react': slateReact, // for backwards compatibility with older plugins + '@grafana/ui': grafanaUI, + 'app/core/app_events': { + default: appEvents, + __useDefault: true, + }, + 'app/core/config': { + default: config, + __useDefault: true, + }, + 'app/core/core': { + appEvents: appEvents, + contextSrv: contextSrv, + }, + 'app/core/services/backend_srv': { + BackendSrv, + getBackendSrv, + }, + 'app/core/table_model': { default: TableModel, __useDefault: true }, + 'app/core/time_series': { default: TimeSeries, __useDefault: true }, + 'app/core/time_series2': { default: TimeSeries, __useDefault: true }, + 'app/core/utils/datemath': grafanaData.dateMath, + 'app/core/utils/flatten': flatten, + 'app/core/utils/kbn': { + default: kbn, + __useDefault: true, + }, + 'app/core/utils/ticks': ticks, + 'app/features/dashboard/impression_store': { + impressions: impressionSrv, + }, + d3: d3, + emotion: emotion, + jquery: { + default: jquery, + __useDefault: true, + }, + ...jQueryFlotDeps, + lodash: { + default: _, + __useDefault: true, + }, + moment: { + default: moment, + __useDefault: true, + }, + prismjs: prismjs, + react: react, + 'react-dom': reactDom, + 'react-redux': reactRedux, + // Migration - React Router v5 -> v6 + // ================================= + // Plugins that still use "react-router-dom@v5" don't depend on react-router directly, so they will not use this import. + // (The react-router-dom@v5 that we expose for them depends on the "react-router" package internally from core.) + // + // Plugins that would like update to "react-router-dom@v6" will need to bundle "react-router-dom", + // however they cannot bundle "react-router" - this would mean that we have two instances of "react-router" + // in the app, which would casue issues. As the "react-router-dom-v5-compat" package re-exports everything from "react-router-dom@v6" + // which then re-exports everything from "react-router@v6", we are in the lucky state to be able to expose a compatible v6 version of the router to plugins by + // just exposing "react-router-dom-v5-compat". + // + // (This means that we are exposing two versions of the same package). + 'react-router-dom': reactRouterDom, // react-router-dom@v5 + 'react-router': reactRouterCompat, // react-router-dom@v6, react-router@v6 (included) + redux: redux, + rxjs: rxjs, + 'rxjs/operators': rxjsOperators, + slate: slate, + 'slate-plain-serializer': slatePlain, + 'slate-react': slateReact, +}; diff --git a/public/app/features/plugins/loader/systemjsHooks.test.ts b/public/app/features/plugins/loader/systemjsHooks.test.ts new file mode 100644 index 00000000000..bc58292aa54 --- /dev/null +++ b/public/app/features/plugins/loader/systemjsHooks.test.ts @@ -0,0 +1,90 @@ +// mock fetch for SystemJS +import 'whatwg-fetch'; + +import { SystemJS, config } from '@grafana/runtime'; + +jest.mock('./cache', () => ({ + resolveWithCache: (url: string) => `${url}?_cache=1234`, +})); + +import { server, mockAmdModule, mockSystemModule } from './pluginLoader.mock'; +import { decorateSystemJSFetch, decorateSystemJSResolve } from './systemjsHooks'; +import { SystemJSWithLoaderHooks } from './types'; + +describe('SystemJS Loader Hooks', () => { + const systemJSPrototype: SystemJSWithLoaderHooks = SystemJS.constructor.prototype; + const originalFetch = systemJSPrototype.fetch; + const originalResolve = systemJSPrototype.resolve; + + systemJSPrototype.resolve = (moduleId: string) => moduleId; + systemJSPrototype.shouldFetch = () => true; + + beforeAll(() => { + server.listen(); + }); + afterEach(() => server.resetHandlers()); + afterAll(() => { + SystemJS.constructor.prototype.resolve = originalResolve; + SystemJS.constructor.prototype.fetch = originalFetch; + server.close(); + }); + + describe('decorateSystemJSFetch', () => { + it('wraps amd module plugins for define function', async () => { + const url = '/public/plugins/my-amd-plugin/module.js'; + const result = await decorateSystemJSFetch(originalFetch, url, {}); + const source = await result.text(); + const expected = `(function(define) { + ${mockAmdModule} +})(window.__grafana_amd_define);`; + + expect(source).toBe(expected); + }); + it("doesn't wrap system module plugins with define function", async () => { + const url = '/public/plugins/my-system-plugin/module.js'; + const result = await decorateSystemJSFetch(originalFetch, url, {}); + const source = await result.text(); + + expect(source).toBe(mockSystemModule); + }); + it('only transforms plugin source code hosted on cdn with cdn paths', async () => { + config.pluginsCDNBaseURL = 'http://my-cdn.com/plugins'; + const cdnUrl = 'http://my-cdn.com/plugins/my-plugin/v1.0.0/public/plugins/my-plugin/module.js'; + const cdnResult = await decorateSystemJSFetch(originalFetch, cdnUrl, {}); + const cdnSource = await cdnResult.text(); + + expect(cdnSource).toContain('var pluginPath = "http://my-cdn.com/plugins/my-plugin/v1.0.0/public/plugins/";'); + + const url = '/public/plugins/my-amd-plugin/module.js'; + const result = await decorateSystemJSFetch(originalFetch, url, {}); + const source = await result.text(); + expect(source).toContain('var pluginPath = "/public/plugins/";'); + }); + }); + describe('decorateSystemJSResolve', () => { + it('removes legacy wildcard from resolved url', () => { + const id = '/public/plugins/my-datasource/styles.css!'; + const result = decorateSystemJSResolve.bind(systemJSPrototype)(originalResolve, id); + + expect(result).toBe('http://localhost/public/plugins/my-datasource/styles.css'); + }); + it('adds default js extension to resolved url', () => { + const id = '/public/plugins/my-plugin/traffic_light'; + const result = decorateSystemJSResolve.bind(systemJSPrototype)(originalResolve, id); + + expect(result).toBe('http://localhost/public/plugins/my-plugin/traffic_light.js'); + }); + it('resolves loadPluginCSS urls correctly', () => { + const id = 'plugins/my-plugin/dark.css'; + const result = decorateSystemJSResolve.bind(systemJSPrototype)(originalResolve, id); + + expect(result).toBe('/public/plugins/my-plugin/dark.css'); + }); + it('adds cache query param to resolved module.js url', () => { + const id = '/public/plugins/my-plugin/module.js'; + const result = decorateSystemJSResolve.bind(systemJSPrototype)(originalResolve, id); + + expect(result).toBe('http://localhost/public/plugins/my-plugin/module.js?_cache=1234'); + }); + }); +}); diff --git a/public/app/features/plugins/loader/systemjsHooks.ts b/public/app/features/plugins/loader/systemjsHooks.ts new file mode 100644 index 00000000000..08023a2ebcd --- /dev/null +++ b/public/app/features/plugins/loader/systemjsHooks.ts @@ -0,0 +1,96 @@ +import { config, SystemJS } from '@grafana/runtime'; + +import { transformPluginSourceForCDN } from '../cdn/utils'; + +import { resolveWithCache } from './cache'; +import { + LOAD_PLUGIN_CSS_REGEX, + JS_CONTENT_TYPE_REGEX, + IS_SYSTEM_MODULE_REGEX, + SHARED_DEPENDENCY_PREFIX, + ENDS_WITH_FILE_EXTENSION_REGEX, +} from './constants'; +import { SystemJSWithLoaderHooks } from './types'; +import { isHostedOnCDN } from './utils'; + +export async function decorateSystemJSFetch( + systemJSFetch: SystemJSWithLoaderHooks['fetch'], + url: string, + options?: Record +) { + const res = await systemJSFetch(url, options); + const contentType = res.headers.get('content-type') || ''; + + if (JS_CONTENT_TYPE_REGEX.test(contentType)) { + const source = await res.text(); + let transformedSrc = source; + if (!IS_SYSTEM_MODULE_REGEX.test(transformedSrc)) { + transformedSrc = preventAMDLoaderCollision(source); + } + + // JS files on the CDN need their asset paths transformed in the source + if (isHostedOnCDN(res.url)) { + const cdnTransformedSrc = transformPluginSourceForCDN({ url: res.url, source: transformedSrc }); + return new Response(new Blob([cdnTransformedSrc], { type: 'text/javascript' })); + } + + return new Response(new Blob([transformedSrc], { type: 'text/javascript' })); + } + return res; +} + +export function decorateSystemJSResolve( + this: SystemJSWithLoaderHooks, + originalResolve: SystemJSWithLoaderHooks['resolve'], + id: string, + parentUrl?: string +) { + const isFileSystemModule = id.endsWith('module.js') && !isHostedOnCDN(id); + + try { + const url = originalResolve.apply(this, [id, parentUrl]); + const cleanedUrl = getBackWardsCompatibleUrl(url); + // Add a cache query param for filesystem module.js requests + // CDN hosted plugins contain the version in the path so skip + return isFileSystemModule ? resolveWithCache(cleanedUrl) : cleanedUrl; + } catch (err) { + // Provide fallback for old plugins that use `loadPluginCss` to load theme styles + // Only affect plugins on the filesystem. + if (LOAD_PLUGIN_CSS_REGEX.test(id)) { + return `${config.appSubUrl ?? ''}/public/${id}`; + } + console.log(`SystemJS: failed to resolve '${id}'`); + return id; + } +} + +export function decorateSystemJsOnload(err: unknown, id: string) { + if (id.endsWith('.css') && !err) { + const module = SystemJS.get(id); + const styles = module?.default; + if (styles) { + document.adoptedStyleSheets = [...document.adoptedStyleSheets, styles]; + } + } +} + +// This function handles the following legacy SystemJS functionality: +// - strips legacy loader wildcard from urls +// - support config.defaultExtension for System.register deps that lack an extension (e.g. './my_ctrl') +function getBackWardsCompatibleUrl(url: string) { + if (url.endsWith('!')) { + url = url.slice(0, -1); + } + const shouldAddDefaultExtension = + !url.startsWith(`${SHARED_DEPENDENCY_PREFIX}:`) && !ENDS_WITH_FILE_EXTENSION_REGEX.test(url); + + return shouldAddDefaultExtension ? url + '.js' : url; +} + +// This transform prevents a conflict between systemjs and requirejs which Monaco Editor +// depends on. See packages/grafana-runtime/src/utils/plugin.ts for more. +function preventAMDLoaderCollision(source: string) { + return `(function(define) { + ${source} +})(window.__grafana_amd_define);`; +} diff --git a/public/app/features/plugins/loader/types.ts b/public/app/features/plugins/loader/types.ts new file mode 100644 index 00000000000..023f6a3986c --- /dev/null +++ b/public/app/features/plugins/loader/types.ts @@ -0,0 +1,7 @@ +// Extend the System type with the loader hooks we use +// to provide backwards compatibility with older version of Systemjs +export type SystemJSWithLoaderHooks = typeof System & { + shouldFetch: () => Boolean; + fetch: (url: string, options?: Record) => Promise; + onload: (err: unknown, id: string) => void; +}; diff --git a/public/app/features/plugins/loader/utils.ts b/public/app/features/plugins/loader/utils.ts new file mode 100644 index 00000000000..84c154dca26 --- /dev/null +++ b/public/app/features/plugins/loader/utils.ts @@ -0,0 +1,24 @@ +import { SystemJS, config } from '@grafana/runtime'; + +import { sandboxPluginDependencies } from '../sandbox/plugin_dependencies'; + +import { SHARED_DEPENDENCY_PREFIX } from './constants'; + +export function buildImportMap(importMap: Record) { + return Object.keys(importMap).reduce>((acc, key) => { + // Use the 'package:' prefix to act as a URL instead of a bare specifier + const module_name = `${SHARED_DEPENDENCY_PREFIX}:${key}`; + // expose dependency to SystemJS + SystemJS.set(module_name, importMap[key]); + + // expose dependency to sandboxed plugins + sandboxPluginDependencies.set(key, importMap[key]); + + acc[key] = module_name; + return acc; + }, {}); +} + +export function isHostedOnCDN(path: string) { + return Boolean(config.pluginsCDNBaseURL) && path.startsWith(config.pluginsCDNBaseURL); +} diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index 29d0e662eb1..8b6d628eef9 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -1,198 +1,46 @@ -import * as emotion from '@emotion/css'; -import * as emotionReact from '@emotion/react'; -import * as d3 from 'd3'; -import jquery from 'jquery'; -import _ from 'lodash'; // eslint-disable-line lodash/import-scope -import moment from 'moment'; // eslint-disable-line no-restricted-imports -import prismjs from 'prismjs'; -import react from 'react'; -import reactDom from 'react-dom'; -import * as reactRedux from 'react-redux'; // eslint-disable-line no-restricted-imports -import * as reactRouterDom from 'react-router-dom'; -import * as reactRouterCompat from 'react-router-dom-v5-compat'; -import * as redux from 'redux'; -import * as rxjs from 'rxjs'; -import * as rxjsOperators from 'rxjs/operators'; -import slate from 'slate'; -import slatePlain from 'slate-plain-serializer'; -import slateReact from 'slate-react'; - -import * as grafanaData from '@grafana/data'; -import * as grafanaRuntime from '@grafana/runtime'; -import * as grafanaUIraw from '@grafana/ui'; -import TableModel from 'app/core/TableModel'; -import config from 'app/core/config'; -import { appEvents, contextSrv } from 'app/core/core'; -import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv'; -import impressionSrv from 'app/core/services/impression_srv'; -import TimeSeries from 'app/core/time_series2'; -import * as flatten from 'app/core/utils/flatten'; -import kbn from 'app/core/utils/kbn'; -import * as ticks from 'app/core/utils/ticks'; +import { + AppPlugin, + DataSourceApi, + DataSourceJsonData, + DataSourcePlugin, + DataSourcePluginMeta, + PluginMeta, +} from '@grafana/data'; +import { SystemJS } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; import { GenericDataSourcePlugin } from '../datasources/types'; import builtInPlugins from './built_in_plugins'; -import { PLUGIN_CDN_URL_KEY } from './constants'; -import { sandboxPluginDependencies } from './sandbox/plugin_dependencies'; +import { registerPluginInCache } from './loader/cache'; +import { sharedDependenciesMap } from './loader/sharedDependencies'; +import { decorateSystemJSFetch, decorateSystemJSResolve, decorateSystemJsOnload } from './loader/systemjsHooks'; +import { SystemJSWithLoaderHooks } from './loader/types'; +import { buildImportMap } from './loader/utils'; import { importPluginModuleInSandbox } from './sandbox/sandbox_plugin_loader'; -import { locateFromCDN, translateForCDN } from './systemjsPlugins/pluginCDN'; -import { fetchCSS, locateCSS } from './systemjsPlugins/pluginCSS'; -import { locateWithCache, registerPluginInCache } from './systemjsPlugins/pluginCacheBuster'; +import { isFrontendSandboxSupported } from './sandbox/utils'; -// Help the 6.4 to 6.5 migration -// The base classes were moved from @grafana/ui to @grafana/data -// This exposes the same classes on both import paths -const grafanaUI = grafanaUIraw as any; -grafanaUI.PanelPlugin = grafanaData.PanelPlugin; -grafanaUI.DataSourcePlugin = grafanaData.DataSourcePlugin; -grafanaUI.AppPlugin = grafanaData.AppPlugin; -grafanaUI.DataSourceApi = grafanaData.DataSourceApi; +const imports = buildImportMap(sharedDependenciesMap); +SystemJS.addImportMap({ imports }); -grafanaRuntime.SystemJS.registry.set('css', grafanaRuntime.SystemJS.newModule({ locate: locateCSS, fetch: fetchCSS })); -grafanaRuntime.SystemJS.registry.set('plugin-loader', grafanaRuntime.SystemJS.newModule({ locate: locateWithCache })); -grafanaRuntime.SystemJS.registry.set( - 'cdn-loader', - grafanaRuntime.SystemJS.newModule({ locate: locateFromCDN, translate: translateForCDN }) -); +const systemJSPrototype: SystemJSWithLoaderHooks = SystemJS.constructor.prototype; -grafanaRuntime.SystemJS.config({ - baseURL: 'public', - defaultExtension: 'js', - packages: { - plugins: { - defaultExtension: 'js', - }, - 'plugin-cdn': { - defaultExtension: 'js', - }, - }, - map: { - text: 'vendor/plugin-text/text.js', - }, - meta: { - '/*': { - esModule: true, - authorization: true, - loader: 'plugin-loader', - }, - '*.css': { - loader: 'css', - }, - [`${PLUGIN_CDN_URL_KEY}/*`]: { - esModule: true, - authorization: false, - loader: 'cdn-loader', - }, - }, -}); +// Monaco Editors reliance on RequireJS means we need to transform +// the content of the plugin code at runtime which can only be done with fetch/eval. +systemJSPrototype.shouldFetch = () => true; -export function exposeToPlugin(name: string, component: any) { - grafanaRuntime.SystemJS.registerDynamic(name, [], true, (require: any, exports: any, module: { exports: any }) => { - module.exports = component; - }); +const systemJSFetch = systemJSPrototype.fetch; +systemJSPrototype.fetch = function (url: string, options?: Record) { + return decorateSystemJSFetch(systemJSFetch, url, options); +}; - // exposes this dependency to sandboxed plugins too. - // the following sandboxPluginDependencies don't depend or interact - // with SystemJS in any way. - sandboxPluginDependencies.set(name, component); -} +const systemJSResolve = systemJSPrototype.resolve; +systemJSPrototype.resolve = decorateSystemJSResolve.bind(systemJSPrototype, systemJSResolve); -exposeToPlugin('@grafana/data', grafanaData); -exposeToPlugin('@grafana/ui', grafanaUI); -exposeToPlugin('@grafana/runtime', grafanaRuntime); -exposeToPlugin('lodash', _); -exposeToPlugin('moment', moment); -exposeToPlugin('jquery', jquery); -exposeToPlugin('d3', d3); -exposeToPlugin('rxjs', rxjs); -exposeToPlugin('rxjs/operators', rxjsOperators); - -// Migration - React Router v5 -> v6 -// ================================= -// Plugins that still use "react-router-dom@v5" don't depend on react-router directly, so they will not use this import. -// (The react-router-dom@v5 that we expose for them depends on the "react-router" package internally from core.) -// -// Plugins that would like update to "react-router-dom@v6" will need to bundle "react-router-dom", -// however they cannot bundle "react-router" - this would mean that we have two instances of "react-router" -// in the app, which would casue issues. As the "react-router-dom-v5-compat" package re-exports everything from "react-router-dom@v6" -// which then re-exports everything from "react-router@v6", we are in the lucky state to be able to expose a compatible v6 version of the router to plugins by -// just exposing "react-router-dom-v5-compat". -// -// (This means that we are exposing two versions of the same package). -exposeToPlugin('react-router', reactRouterCompat); // react-router-dom@v6, react-router@v6 (included) -exposeToPlugin('react-router-dom', reactRouterDom); // react-router-dom@v5 - -// Experimental modules -exposeToPlugin('prismjs', prismjs); -exposeToPlugin('slate', slate); -exposeToPlugin('slate-react', slateReact); -exposeToPlugin('@grafana/slate-react', slateReact); // for backwards compatibility with older plugins -exposeToPlugin('slate-plain-serializer', slatePlain); -exposeToPlugin('react', react); -exposeToPlugin('react-dom', reactDom); -exposeToPlugin('react-redux', reactRedux); -exposeToPlugin('redux', redux); -exposeToPlugin('emotion', emotion); -exposeToPlugin('@emotion/css', emotion); -exposeToPlugin('@emotion/react', emotionReact); - -exposeToPlugin('app/features/dashboard/impression_store', { - impressions: impressionSrv, - __esModule: true, -}); - -/** - * NOTE: this is added temporarily while we explore a long term solution - * If you use this export, only use the: - * get/delete/post/patch/request methods - */ -exposeToPlugin('app/core/services/backend_srv', { - BackendSrv, - getBackendSrv, -}); - -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/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', { - appEvents: appEvents, - contextSrv: contextSrv, - __esModule: true, -}); - -import 'vendor/flot/jquery.flot'; -import 'vendor/flot/jquery.flot.selection'; -import 'vendor/flot/jquery.flot.time'; -import 'vendor/flot/jquery.flot.stack'; -import 'vendor/flot/jquery.flot.stackpercent'; -import 'vendor/flot/jquery.flot.fillbelow'; -import 'vendor/flot/jquery.flot.crosshair'; -import 'vendor/flot/jquery.flot.dashes'; -import 'vendor/flot/jquery.flot.gauge'; - -const flotDeps = [ - 'jquery.flot', - 'jquery.flot.pie', - 'jquery.flot.time', - 'jquery.flot.fillbelow', - 'jquery.flot.crosshair', - 'jquery.flot.stack', - 'jquery.flot.selection', - 'jquery.flot.stackpercent', - 'jquery.flot.events', - 'jquery.flot.gauge', -]; - -for (const flotDep of flotDeps) { - exposeToPlugin(flotDep, { fakeDep: 1 }); -} +// Older plugins load .css files which resolves to a CSS Module. +// https://github.com/WICG/webcomponents/blob/gh-pages/proposals/css-modules-v1-explainer.md#importing-a-css-module +// Any css files loaded via SystemJS have their styles applied onload. +systemJSPrototype.onload = decorateSystemJsOnload; export async function importPluginModule({ path, @@ -204,7 +52,7 @@ export async function importPluginModule({ pluginId: string; version?: string; isAngular?: boolean; -}): Promise { +}): Promise { if (version) { registerPluginInCache({ path, version }); } @@ -224,23 +72,10 @@ export async function importPluginModule({ return importPluginModuleInSandbox({ pluginId }); } - return grafanaRuntime.SystemJS.import(path); + return SystemJS.import(path); } -function isFrontendSandboxSupported({ isAngular, pluginId }: { isAngular?: boolean; pluginId: string }): boolean { - // To fast test and debug the sandbox in the browser. - const sandboxQueryParam = location.search.includes('nosandbox') && config.buildInfo.env === 'development'; - const isPluginExcepted = config.disableFrontendSandboxForPlugins.includes(pluginId); - return ( - !isAngular && - Boolean(config.featureToggles.pluginsFrontendSandbox) && - process.env.NODE_ENV !== 'test' && - !isPluginExcepted && - !sandboxQueryParam - ); -} - -export function importDataSourcePlugin(meta: grafanaData.DataSourcePluginMeta): Promise { +export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise { return importPluginModule({ path: meta.module, version: meta.info?.version, @@ -248,16 +83,16 @@ export function importDataSourcePlugin(meta: grafanaData.DataSourcePluginMeta): pluginId: meta.id, }).then((pluginExports) => { if (pluginExports.plugin) { - const dsPlugin = pluginExports.plugin as GenericDataSourcePlugin; + const dsPlugin: GenericDataSourcePlugin = pluginExports.plugin; dsPlugin.meta = meta; return dsPlugin; } if (pluginExports.Datasource) { - const dsPlugin = new grafanaData.DataSourcePlugin< - grafanaData.DataSourceApi, - grafanaData.DataQuery, - grafanaData.DataSourceJsonData + const dsPlugin = new DataSourcePlugin< + DataSourceApi, + DataQuery, + DataSourceJsonData >(pluginExports.Datasource); dsPlugin.setComponentsFromLegacyExports(pluginExports); dsPlugin.meta = meta; @@ -268,14 +103,14 @@ export function importDataSourcePlugin(meta: grafanaData.DataSourcePluginMeta): }); } -export function importAppPlugin(meta: grafanaData.PluginMeta): Promise { +export function importAppPlugin(meta: PluginMeta): Promise { return importPluginModule({ path: meta.module, version: meta.info?.version, isAngular: meta.angularDetected, pluginId: meta.id, }).then((pluginExports) => { - const plugin = pluginExports.plugin ? (pluginExports.plugin as grafanaData.AppPlugin) : new grafanaData.AppPlugin(); + const plugin: AppPlugin = pluginExports.plugin ? pluginExports.plugin : new AppPlugin(); plugin.init(meta); plugin.meta = meta; plugin.setComponentsFromLegacyExports(pluginExports); diff --git a/public/app/features/plugins/sandbox/code_loader.ts b/public/app/features/plugins/sandbox/code_loader.ts index 3c2ff92a5d8..e882341b893 100644 --- a/public/app/features/plugins/sandbox/code_loader.ts +++ b/public/app/features/plugins/sandbox/code_loader.ts @@ -1,7 +1,7 @@ import { PluginMeta } from '@grafana/data'; -import { getPluginCdnResourceUrl, extractPluginIdVersionFromUrl, transformPluginSourceForCDN } from '../cdn/utils'; -import { PLUGIN_CDN_URL_KEY } from '../constants'; +import { transformPluginSourceForCDN } from '../cdn/utils'; +import { isHostedOnCDN } from '../loader/utils'; import { SandboxEnvironment } from './types'; @@ -21,15 +21,13 @@ export async function loadScriptIntoSandbox(url: string, meta: PluginMeta, sandb scriptCode = patchPluginSourceMap(meta, scriptCode); // cdn loaded - } else if (url.includes(PLUGIN_CDN_URL_KEY)) { + } else if (isHostedOnCDN(url)) { const response = await fetch(url); scriptCode = await response.text(); - const pluginUrl = getPluginCdnResourceUrl(`/public/${meta.module}`) + '.js'; - const { version } = extractPluginIdVersionFromUrl(pluginUrl); scriptCode = transformPluginSourceForCDN({ - pluginId: meta.id, - version, + url, source: scriptCode, + transformSourceMapURL: true, }); } @@ -41,16 +39,15 @@ export async function loadScriptIntoSandbox(url: string, meta: PluginMeta, sandb } export async function getPluginCode(meta: PluginMeta): Promise { - if (meta.module.includes(`${PLUGIN_CDN_URL_KEY}/`)) { + if (isHostedOnCDN(meta.module)) { // should load plugin from a CDN - const pluginUrl = getPluginCdnResourceUrl(`/public/${meta.module}`) + '.js'; - const response = await fetch(pluginUrl); + const url = meta.module; + const response = await fetch(url); let pluginCode = await response.text(); - const { version } = extractPluginIdVersionFromUrl(pluginUrl); pluginCode = transformPluginSourceForCDN({ - pluginId: meta.id, - version, + url, source: pluginCode, + transformSourceMapURL: true, }); return pluginCode; } else { @@ -84,7 +81,7 @@ function patchPluginSourceMap(meta: PluginMeta, pluginCode: string): string { replaceWith += `//# sourceURL=module.js\n`; } // modify the source map url to point to the correct location - const sourceCodeMapUrl = `/public/${meta.module}.js.map`; + const sourceCodeMapUrl = `/public/${meta.module}.map`; replaceWith += `//# sourceMappingURL=${sourceCodeMapUrl}`; return pluginCode.replace('//# sourceMappingURL=module.js.map', replaceWith); diff --git a/public/app/features/plugins/sandbox/sandbox_components.tsx b/public/app/features/plugins/sandbox/sandbox_components.tsx index 0da4c534456..671c37e38f6 100644 --- a/public/app/features/plugins/sandbox/sandbox_components.tsx +++ b/public/app/features/plugins/sandbox/sandbox_components.tsx @@ -26,9 +26,9 @@ import { isSandboxedPluginObject } from './utils'; * */ export async function sandboxPluginComponents( - pluginExports: unknown, + pluginExports: System.Module, meta: PluginMeta -): Promise { +): Promise { if (!isSandboxedPluginObject(pluginExports)) { // we should monitor these cases. There should not be any plugins without a plugin export loaded inside the sandbox return pluginExports; diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts index 42bec39b023..df2db7d9962 100644 --- a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts +++ b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts @@ -24,15 +24,15 @@ if (process.env.NODE_ENV !== 'production') { require('@locker/near-membrane-dom/custom-devtools-formatter'); } -const pluginImportCache = new Map>(); +const pluginImportCache = new Map>(); -export async function importPluginModuleInSandbox({ pluginId }: { pluginId: string }): Promise { +export async function importPluginModuleInSandbox({ pluginId }: { pluginId: string }): Promise { try { const pluginMeta = await getPluginSettings(pluginId); if (!pluginImportCache.has(pluginId)) { pluginImportCache.set(pluginId, doImportPluginModuleInSandbox(pluginMeta)); } - return pluginImportCache.get(pluginId); + return pluginImportCache.get(pluginId)!; } catch (e) { const error = new Error(`Could not import plugin ${pluginId} inside sandbox: ` + e); logError(error, { @@ -43,7 +43,7 @@ export async function importPluginModuleInSandbox({ pluginId }: { pluginId: stri } } -async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise { +async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise { return new Promise(async (resolve, reject) => { const generalDistortionMap = getGeneralSandboxDistortionMap(); let sandboxEnvironment: SandboxEnvironment; diff --git a/public/app/features/plugins/sandbox/utils.ts b/public/app/features/plugins/sandbox/utils.ts index fccee702128..74305e594e4 100644 --- a/public/app/features/plugins/sandbox/utils.ts +++ b/public/app/features/plugins/sandbox/utils.ts @@ -38,6 +38,25 @@ export function logError(error: Error, context?: LogContext) { logErrorRuntime(error, context); } +export function isFrontendSandboxSupported({ + isAngular, + pluginId, +}: { + isAngular?: boolean; + pluginId: string; +}): boolean { + // To fast test and debug the sandbox in the browser. + const sandboxQueryParam = location.search.includes('nosandbox') && config.buildInfo.env === 'development'; + const isPluginExcepted = config.disableFrontendSandboxForPlugins.includes(pluginId); + return ( + !isAngular && + Boolean(config.featureToggles.pluginsFrontendSandbox) && + process.env.NODE_ENV !== 'test' && + !isPluginExcepted && + !sandboxQueryParam + ); +} + function isRegex(value: unknown): value is RegExp { return value?.constructor?.name === 'RegExp'; } diff --git a/public/app/features/plugins/systemjsPlugins/pluginCDN.ts b/public/app/features/plugins/systemjsPlugins/pluginCDN.ts deleted file mode 100644 index 8e3784e8d0d..00000000000 --- a/public/app/features/plugins/systemjsPlugins/pluginCDN.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { extractPluginIdVersionFromUrl, getPluginCdnResourceUrl, transformPluginSourceForCDN } from '../cdn/utils'; - -import type { SystemJSLoad } from './types'; - -/* - Locate: Overrides the location of the plugin resource - Plugins loaded via CDN fall into this plugin via the `plugin-cdn` keyword. - Systemjs first resolves to an origin on the local filesystem - (e.g. http://localhost/public/plugin-cdn/{pluginId}/{version}/public/plugins/{pluginId}) - we then split this url and prefix with the CDN base url giving us the correct asset location. - */ -export function locateFromCDN(load: SystemJSLoad) { - const { address } = load; - return getPluginCdnResourceUrl(address); -} - -/* - Translate: Returns the translated source from load.source; - */ -export function translateForCDN(load: SystemJSLoad) { - const { id, version } = extractPluginIdVersionFromUrl(load.name); - return transformPluginSourceForCDN({ pluginId: id, version, source: load.source }); -} diff --git a/public/app/features/plugins/systemjsPlugins/pluginCSS.ts b/public/app/features/plugins/systemjsPlugins/pluginCSS.ts deleted file mode 100644 index a97002ab8ed..00000000000 --- a/public/app/features/plugins/systemjsPlugins/pluginCSS.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { noop } from 'lodash'; - -import { config } from '@grafana/runtime'; - -import type { SystemJSLoad } from './types'; - -/* - Locate: Overrides the location of the plugin resource - Plugins that import css use relative paths in Systemjs.register dependency list. - Rather than attempt to resolve it in the pluginCDN systemjs plugin let SystemJS resolve it to origin - then we can replace the "baseUrl" with the "cdnHost". - */ -export function locateCSS(load: SystemJSLoad) { - if (load.metadata.loader === 'cdn-loader' && load.address.startsWith(`${location.origin}/public/plugin-cdn`)) { - load.address = load.address.replace(`${location.origin}/public/plugin-cdn`, config.pluginsCDNBaseURL); - } - return load.address; -} - -/* - Fetch: Called with second argument representing default fetch function, has full control of fetch output. - Plugins that have external CSS will use this plugin to load their custom styles -*/ -export function fetchCSS(load: SystemJSLoad) { - const links = document.getElementsByTagName('link'); - const linkHrefs: string[] = Array.from(links).map((link) => link.href); - - // dont reload styles loaded in the head - if (linkHrefs.includes(load.address)) { - return ''; - } - - return loadCSS(load.address); -} - -const bust = '?_cache=' + Date.now(); -const waitSeconds = 100; - -function loadCSS(url: string) { - return new Promise(function (resolve, reject) { - const timeout = setTimeout(function () { - reject('Unable to load CSS'); - }, waitSeconds * 1000); - const _callback = function (error?: string | Error) { - clearTimeout(timeout); - link.onload = link.onerror = noop; - setTimeout(function () { - if (error) { - reject(error); - } else { - resolve(''); - } - }, 7); - }; - const link = document.createElement('link'); - link.type = 'text/css'; - link.rel = 'stylesheet'; - link.href = url; - - // Don't cache bust plugins loaded from cdn. - if (!link.href.startsWith(config.pluginsCDNBaseURL)) { - link.href = link.href + bust; - } - - link.onload = function () { - _callback(); - }; - - link.onerror = function (event) { - _callback(event instanceof ErrorEvent ? event.message : new Error('Error loading CSS file.')); - }; - - document.head.appendChild(link); - }); -} diff --git a/public/app/features/plugins/systemjsPlugins/types.ts b/public/app/features/plugins/systemjsPlugins/types.ts deleted file mode 100644 index 3a7356084d0..00000000000 --- a/public/app/features/plugins/systemjsPlugins/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -export type SystemJSLoad = { - address: string; - metadata: { - authorization: boolean; - cjsDeferDepsExecute: boolean; - cjsRequireDetection: boolean; - crossOrigin?: boolean; - encapsulateGlobal: boolean; - esModule: boolean; - integrity?: string; - loader: string; - scriptLoad?: boolean; - }; - name: string; - source: string; -}; diff --git a/public/app/features/plugins/tests/plugin_loader.test.ts b/public/app/features/plugins/tests/plugin_loader.test.ts index ab94ea5f960..a5800883dca 100644 --- a/public/app/features/plugins/tests/plugin_loader.test.ts +++ b/public/app/features/plugins/tests/plugin_loader.test.ts @@ -12,7 +12,7 @@ jest.mock('app/core/core', () => { import { AppPluginMeta, PluginMetaInfo, PluginType, AppPlugin } from '@grafana/data'; import { SystemJS } from '@grafana/runtime'; -// Loaded after the `unmock` abve +// Loaded after the `unmock` above import { importAppPlugin } from '../plugin_loader'; class MyCustomApp extends AppPlugin { @@ -27,14 +27,18 @@ class MyCustomApp extends AppPlugin { describe('Load App', () => { const app = new MyCustomApp(); - const modulePath = 'my/custom/plugin/module'; + const modulePath = 'http://localhost:3000/public/plugins/my-app-plugin/module.js'; + // Hook resolver for tests + const originalResolve = SystemJS.constructor.prototype.resolve; + SystemJS.constructor.prototype.resolve = (x: unknown) => x; beforeAll(() => { - SystemJS.set(modulePath, SystemJS.newModule({ plugin: app })); + SystemJS.set(modulePath, { plugin: app }); }); afterAll(() => { SystemJS.delete(modulePath); + SystemJS.constructor.prototype.resolve = originalResolve; }); it('should call init and set meta', async () => { diff --git a/public/app/types/window.d.ts b/public/app/types/window.d.ts index 4d9abb68d44..0365573751a 100644 --- a/public/app/types/window.d.ts +++ b/public/app/types/window.d.ts @@ -5,6 +5,7 @@ export declare global { __grafana_load_failed: () => void; public_cdn_path: string; nonce: string | undefined; + System: typeof System; } // Augment DOMParser to accept TrustedType sanitised content diff --git a/public/test/mocks/systemjsAMDExtra.ts b/public/test/mocks/systemjsAMDExtra.ts new file mode 100644 index 00000000000..9545549058b --- /dev/null +++ b/public/test/mocks/systemjsAMDExtra.ts @@ -0,0 +1,5 @@ +// the systemjs amd extra is required for loading AMD formatted plugins at runtime +// however it makes changes to global.define which breaks tests with errors similar to: +// TypeError: tslib_1.__importDefault is not a function +// +export const systemjsAMDExtra = 'systemjsAMDExtra'; diff --git a/yarn.lock b/yarn.lock index 1404d823352..079f74c49e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3915,7 +3915,7 @@ __metadata: "@types/lodash": 4.14.195 "@types/react": 18.2.15 "@types/react-dom": 18.2.7 - "@types/systemjs": ^0.20.6 + "@types/systemjs": 6.13.1 esbuild: 0.18.12 history: 4.10.1 lodash: 4.17.21 @@ -3929,7 +3929,8 @@ __metadata: rollup-plugin-sourcemaps: 0.6.3 rollup-plugin-terser: 7.0.2 rxjs: 7.8.1 - systemjs: 0.20.19 + systemjs: 6.14.2 + systemjs-cjs-extra: 0.1.0 tslib: 2.6.0 typescript: 4.8.4 peerDependencies: @@ -10792,10 +10793,10 @@ __metadata: languageName: node linkType: hard -"@types/systemjs@npm:^0.20.6": - version: 0.20.8 - resolution: "@types/systemjs@npm:0.20.8" - checksum: 10bf85416521b89cd20c6eddd72f7414ab6eff31ff8c577152d6f5dd952daf7f9c5eed32d3c482ddbfbe2e9586ed5287838b6dc8a7d6b3963fdad8afbece717b +"@types/systemjs@npm:6.13.1": + version: 6.13.1 + resolution: "@types/systemjs@npm:6.13.1" + checksum: 61fad0cbdbc61e0b6c6fd82d9623d9e68ff587848e0c8198a6bb4a78985b4880962543e02aeb96dfdb4623439ef0d848fde61a82757308dd5067040deee9d1ee languageName: node linkType: hard @@ -19350,6 +19351,7 @@ __metadata: "@types/slate": 0.47.11 "@types/slate-plain-serializer": 0.7.2 "@types/slate-react": 0.22.9 + "@types/systemjs": 6.13.1 "@types/testing-library__jest-dom": 5.14.8 "@types/tinycolor2": 1.4.3 "@types/trusted-types": 2.0.3 @@ -30704,10 +30706,17 @@ __metadata: languageName: node linkType: hard -"systemjs@npm:0.20.19": - version: 0.20.19 - resolution: "systemjs@npm:0.20.19" - checksum: 4b5a404be790cd545d6994f617c24ef3a3ae52e029c6498f10206f715c6777c4c0733ab9d626723fdfa359f12707835f302986516e5835be26c202bc13e242df +"systemjs-cjs-extra@npm:0.1.0": + version: 0.1.0 + resolution: "systemjs-cjs-extra@npm:0.1.0" + checksum: 09169416c858e43671e988f05f2c14fe2fa923472619983fce44aa946493337bdda0a6c28ea8aeded0c2d3dfa62ad6d8479944144b451c0492f7a505e7b18813 + languageName: node + linkType: hard + +"systemjs@npm:6.14.2": + version: 6.14.2 + resolution: "systemjs@npm:6.14.2" + checksum: 1f58c7da8f7deb8e4a3eb357e67f5363eb97e44a456f8f81ece391f2e1ae54ac70fb62135043d4cf96c11c6b76d7963c295ea844ca25bcb3b55193f6727fd0b2 languageName: node linkType: hard