mirror of
https://github.com/grafana/grafana.git
synced 2025-01-24 15:27:01 -06:00
Plugins: Bump SystemJS to 6.14.2 (#70068)
* chore(plugins): bump systemjs to latest version * refactor(plugins): switch runtime over to use latest systemjs, add typings * refactor(plugins): use latest systemjs APIs for runtime dependency resolution * refactor(plugins): return valid urls from backend for latest systemjs import to work * revert(plugins): remove cfg from assetpath in favour of relative paths * fix(plugins): useDefault for systemjs deps to solve undefined errors * feat(plugins): add basic support for loading plugins via CDN * fix(plugins): load nested plugins with latest systemjs * feat(plugins): add back ability to transform plugin src for cdns * feat(plugins): get caching for module.js working, clean up * refactor(plugin_loader): create buildImportMap fn and more clean up * refactor(angularapp): use buildImportMap for dependencies and clean up * test(plugin_loader): fix failing test due to systemjs update * test(jest): mock systemjs amd extra in tests to prevent it breaking tests * chore(plugins): remove systemjs module-types extra, already included with system * fix(plugins): update key for invalidating cached plugins * fix: move systemjs amd define to another global so monaco can load * refactor(plugins): clean up cache buster and tests * chore(plugins): remove debug * refactor(plugins): move systemjs define to keep global cleaner * fix(plugins): set useDefault so system modules lodash references dont fail * feat(plugins): hook systemjs onload so stylesheets are applied to the dom * refactor(plugins): wrap amd formatted plugins in iife to prevent define collision * feat(plugins): support system module format for legacy plugins * test(plugincachebuster): update tests to match latest implementation * test(plugins-loader): fix up tests post module property change * fix test * Update pkg/plugins/manager/loader/assetpath/assetpath.go Co-authored-by: Will Browne <wbrowne@users.noreply.github.com> * chore(plugin_loader): remove stray import from merge conflict * Revert "Update pkg/plugins/manager/loader/assetpath/assetpath.go" This reverts commit 0df57d1cf20f49c22c93369001c70aae46a97c42. * fix(plugin_loader): set use default for shared plugin dependencies * refactor(plugins): use leading slash for Module and BaseUrl * fix(plugins): fix resolve appending extension to cache query param * refactor(plugins): align baseurl and module paths * refactor(plugins): update builtInPlugins keys to match naming convention * refactor(plugins): minor loader clean up, fix up types * test(plugins): fix failing tests * refactor(plugins): rename cache buster systemjs plugin to cache * refactor(plugins): separate plugin_loader into smaller files * chore(plugins): clean up plugin_loader types * chore(plugin_sandbox): fix typescript error * chore(npm): remove unplug debug and pin systemjs to 6.14.1 * refactor(plugins-cdn): update loaders to use absolute module url from backend * fix(plugins): escape period in systemjs module regex * chore(plugins): delete redundant systemjs plugins * refactor(plugin_loader): move hooks into own file, add types * test(plugins): add tests for systemjs loader hooks * chore(plugins): rename systemjshooks file * chore(plugins): remove redundant systemjs cdn backend code * fix(plugins): handle loading with config.appSubUrl * chore(plugins): delete redundant plugin-cdn angular code * test(plugins): fix failing systemjs test missing pluginsCDNBaseUrl * refactor(plugins): backend provides base and module properties with AppSubUrl * fix(plugins): consider AppSubUrl for plugin logos * fix(plugins): use isHostedOnCDN util when checking for cdn hosted plugins * add new appSubURL field to config * refactor relative URL func * fix path for core app * refactor asset path input * fix(plugins): catch errors in loadPluginCss * feat(plugins-cdn): selectively transform sourceMapURL * re-add deleted test case * chore(plugins): bump to latest systemjs@6.14.2 * feat(plugins): add systemjs-cjs-extra for loading commonjs plugins --------- Co-authored-by: Will Browne <will.browne@grafana.com> Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
This commit is contained in:
parent
b2f7476bb4
commit
62821c69b3
@ -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"]
|
||||
],
|
||||
|
@ -41,6 +41,8 @@ module.exports = {
|
||||
// near-membrane-dom won't work in a nodejs environment.
|
||||
'@locker/near-membrane-dom': '<rootDir>/public/test/mocks/nearMembraneDom.ts',
|
||||
'^@grafana/schema/dist/esm/(.*)$': '<rootDir>/packages/grafana-schema/src/$1',
|
||||
// prevent systemjs amd extra from breaking tests.
|
||||
'systemjs/dist/extras/amd': '<rootDir>/public/test/mocks/systemjsAMDExtra.ts',
|
||||
},
|
||||
// Log the test results with dynamic Loki tags. Drone CI only
|
||||
reporters: ['default', ['<rootDir>/public/test/log-reporter.js', { enable: process.env.DRONE === 'true' }]],
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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<any> {
|
||||
const theme = config.bootData.user.lightTheme ? options.light : options.dark;
|
||||
return SystemJS.import(`${theme}!css`);
|
||||
export async function loadPluginCss(options: PluginCssOptions): Promise<any> {
|
||||
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;
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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)))
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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{
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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<string, System.Module>;
|
||||
|
||||
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;
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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) => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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<string, System.Module | (() => Promise<System.Module>)> = {
|
||||
// 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;
|
||||
|
@ -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 = "<img src='http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/data/myimage.jpg'>";
|
||||
`;
|
||||
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 = "<img src='http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/data/myimage.jpg'>";
|
||||
`;
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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]}`;
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
export const PLUGIN_CDN_URL_KEY = 'plugin-cdn';
|
@ -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`;
|
||||
}
|
@ -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;
|
||||
}
|
5
public/app/features/plugins/loader/constants.ts
Normal file
5
public/app/features/plugins/loader/constants.ts
Normal file
@ -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\(/;
|
37
public/app/features/plugins/loader/pluginLoader.mock.ts
Normal file
37
public/app/features/plugins/loader/pluginLoader.mock.ts
Normal file
@ -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 };
|
140
public/app/features/plugins/loader/sharedDependencies.ts
Normal file
140
public/app/features/plugins/loader/sharedDependencies.ts
Normal file
@ -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<string, unknown> = 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<string, System.Module> = {
|
||||
'@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,
|
||||
};
|
90
public/app/features/plugins/loader/systemjsHooks.test.ts
Normal file
90
public/app/features/plugins/loader/systemjsHooks.test.ts
Normal file
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
96
public/app/features/plugins/loader/systemjsHooks.ts
Normal file
96
public/app/features/plugins/loader/systemjsHooks.ts
Normal file
@ -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<string, unknown>
|
||||
) {
|
||||
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);`;
|
||||
}
|
7
public/app/features/plugins/loader/types.ts
Normal file
7
public/app/features/plugins/loader/types.ts
Normal file
@ -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<string, unknown>) => Promise<Response>;
|
||||
onload: (err: unknown, id: string) => void;
|
||||
};
|
24
public/app/features/plugins/loader/utils.ts
Normal file
24
public/app/features/plugins/loader/utils.ts
Normal file
@ -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<string, System.Module>) {
|
||||
return Object.keys(importMap).reduce<Record<string, string>>((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);
|
||||
}
|
@ -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<string, unknown>) {
|
||||
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<any> {
|
||||
}): Promise<System.Module> {
|
||||
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<GenericDataSourcePlugin> {
|
||||
export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise<GenericDataSourcePlugin> {
|
||||
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>,
|
||||
grafanaData.DataQuery,
|
||||
grafanaData.DataSourceJsonData
|
||||
const dsPlugin = new DataSourcePlugin<
|
||||
DataSourceApi<DataQuery, DataSourceJsonData>,
|
||||
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<grafanaData.AppPlugin> {
|
||||
export function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> {
|
||||
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);
|
||||
|
@ -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<string> {
|
||||
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);
|
||||
|
@ -26,9 +26,9 @@ import { isSandboxedPluginObject } from './utils';
|
||||
*
|
||||
*/
|
||||
export async function sandboxPluginComponents(
|
||||
pluginExports: unknown,
|
||||
pluginExports: System.Module,
|
||||
meta: PluginMeta
|
||||
): Promise<SandboxedPluginObject | unknown> {
|
||||
): Promise<SandboxedPluginObject | System.Module> {
|
||||
if (!isSandboxedPluginObject(pluginExports)) {
|
||||
// we should monitor these cases. There should not be any plugins without a plugin export loaded inside the sandbox
|
||||
return pluginExports;
|
||||
|
@ -24,15 +24,15 @@ if (process.env.NODE_ENV !== 'production') {
|
||||
require('@locker/near-membrane-dom/custom-devtools-formatter');
|
||||
}
|
||||
|
||||
const pluginImportCache = new Map<string, Promise<unknown>>();
|
||||
const pluginImportCache = new Map<string, Promise<System.Module>>();
|
||||
|
||||
export async function importPluginModuleInSandbox({ pluginId }: { pluginId: string }): Promise<unknown> {
|
||||
export async function importPluginModuleInSandbox({ pluginId }: { pluginId: string }): Promise<System.Module> {
|
||||
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<unknown> {
|
||||
async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise<System.Module> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const generalDistortionMap = getGeneralSandboxDistortionMap();
|
||||
let sandboxEnvironment: SandboxEnvironment;
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -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 });
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
@ -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;
|
||||
};
|
@ -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 () => {
|
||||
|
1
public/app/types/window.d.ts
vendored
1
public/app/types/window.d.ts
vendored
@ -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
|
||||
|
5
public/test/mocks/systemjsAMDExtra.ts
Normal file
5
public/test/mocks/systemjsAMDExtra.ts
Normal file
@ -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';
|
29
yarn.lock
29
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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user