Plugins: Support for link extensions (#61663)

* added extensions to plugin.json and exposing it via frontend settings.

* added extensions to the plugin.json schema.

* changing the extensions in frontend settings to a map instead of an array.

* wip

* feat(pluginregistry): begin wiring up registry

* feat(pluginextensions): prevent duplicate links and clean up

* added test case for link extensions.

* added tests and implemented the getPluginLink function.

* wip

* feat(pluginextensions): expose plugin extension registry

* fix(pluginextensions): appease the typescript gods post rename

* renamed file and will throw error if trying to call setExtensionsRegistry if trying to call it twice.

* added reafactorings.

* fixed failing test.

* minor refactorings to make sure we only include extensions if the app is enabled.

* fixed some nits.

* Update public/app/features/plugins/extensions/registry.test.ts

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

* Update packages/grafana-runtime/src/services/pluginExtensions/registry.ts

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

* Update packages/grafana-runtime/src/services/pluginExtensions/registry.ts

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

* Update public/app/features/plugins/extensions/registry.test.ts

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

* Moved types for extensions from data to runtime.

* added a small example on how you could consume link extensions.

* renamed after feedback from levi.

* updated the plugindef.cue.

* using the generated plugin def.

* added tests for apps and extensions.

* fixed linting issues.

* wip

* wip

* wip

* wip

* test(extensions): fix up failing tests

* feat(extensions): freeze registry extension arrays, include type in registry items

* added restrictions in the pugindef cue schema.

* wip

* added required fields.

* added key to uniquely identify each item.

* test(pluginextensions): align tests with implementation

* chore(schema): refresh reference.md

---------

Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
This commit is contained in:
Marcus Andersson
2023-02-07 17:20:05 +01:00
committed by GitHub
co-authored by Levente Balogh Jack Westbrook
parent 8a94688114
commit 1cfd3f81fb
24 changed files with 812 additions and 98 deletions
+15 -15
View File
@@ -126,6 +126,7 @@ type FrontendSettingsDTO struct {
Datasources map[string]plugins.DataSourceDTO `json:"datasources"`
MinRefreshInterval string `json:"minRefreshInterval"`
Panels map[string]plugins.PanelDTO `json:"panels"`
Apps map[string]*plugins.AppDTO `json:"apps"`
AppUrl string `json:"appUrl"`
AppSubUrl string `json:"appSubUrl"`
AllowOrgCreate bool `json:"allowOrgCreate"`
@@ -158,21 +159,20 @@ type FrontendSettingsDTO struct {
RudderstackSdkUrl string `json:"rudderstackSdkUrl"`
RudderstackConfigUrl string `json:"rudderstackConfigUrl"`
FeedbackLinksEnabled bool `json:"feedbackLinksEnabled"`
ApplicationInsightsConnectionString string `json:"applicationInsightsConnectionString"`
ApplicationInsightsEndpointUrl string `json:"applicationInsightsEndpointUrl"`
DisableLoginForm bool `json:"disableLoginForm"`
DisableUserSignUp bool `json:"disableUserSignUp"`
LoginHint string `json:"loginHint"`
PasswordHint string `json:"passwordHint"`
ExternalUserMngInfo string `json:"externalUserMngInfo"`
ExternalUserMngLinkUrl string `json:"externalUserMngLinkUrl"`
ExternalUserMngLinkName string `json:"externalUserMngLinkName"`
ViewersCanEdit bool `json:"viewersCanEdit"`
AngularSupportEnabled bool `json:"angularSupportEnabled"`
EditorsCanAdmin bool `json:"editorsCanAdmin"`
DisableSanitizeHtml bool `json:"disableSanitizeHtml"`
PluginsToPreload []*plugins.PreloadPlugin `json:"pluginsToPreload"`
FeedbackLinksEnabled bool `json:"feedbackLinksEnabled"`
ApplicationInsightsConnectionString string `json:"applicationInsightsConnectionString"`
ApplicationInsightsEndpointUrl string `json:"applicationInsightsEndpointUrl"`
DisableLoginForm bool `json:"disableLoginForm"`
DisableUserSignUp bool `json:"disableUserSignUp"`
LoginHint string `json:"loginHint"`
PasswordHint string `json:"passwordHint"`
ExternalUserMngInfo string `json:"externalUserMngInfo"`
ExternalUserMngLinkUrl string `json:"externalUserMngLinkUrl"`
ExternalUserMngLinkName string `json:"externalUserMngLinkName"`
ViewersCanEdit bool `json:"viewersCanEdit"`
AngularSupportEnabled bool `json:"angularSupportEnabled"`
EditorsCanAdmin bool `json:"editorsCanAdmin"`
DisableSanitizeHtml bool `json:"disableSanitizeHtml"`
Auth FrontendSettingsAuthDTO `json:"auth"`
+70 -41
View File
@@ -31,22 +31,20 @@ func (hs *HTTPServer) GetFrontendSettings(c *contextmodel.ReqContext) {
// getFrontendSettings returns a json object with all the settings needed for front end initialisation.
func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.FrontendSettingsDTO, error) {
enabledPlugins, err := hs.enabledPlugins(c.Req.Context(), c.OrgID)
availablePlugins, err := hs.availablePlugins(c.Req.Context(), c.OrgID)
if err != nil {
return nil, err
}
pluginsToPreload := make([]*plugins.PreloadPlugin, 0)
for _, app := range enabledPlugins[plugins.App] {
if app.Preload {
pluginsToPreload = append(pluginsToPreload, &plugins.PreloadPlugin{
Path: app.Module,
Version: app.Info.Version,
})
}
apps := make(map[string]*plugins.AppDTO, 0)
for _, ap := range availablePlugins[plugins.App] {
apps[ap.Plugin.ID] = newAppDTO(
ap.Plugin,
ap.Settings,
)
}
dataSources, err := hs.getFSDataSources(c, enabledPlugins)
dataSources, err := hs.getFSDataSources(c, availablePlugins)
if err != nil {
return nil, err
}
@@ -59,7 +57,8 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
}
panels := make(map[string]plugins.PanelDTO)
for _, panel := range enabledPlugins[plugins.Panel] {
for _, ap := range availablePlugins[plugins.Panel] {
panel := ap.Plugin
if panel.State == plugins.AlphaRelease && !hs.Cfg.PluginsEnableAlpha {
continue
}
@@ -102,6 +101,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
Datasources: dataSources,
MinRefreshInterval: setting.MinRefreshInterval,
Panels: panels,
Apps: apps,
AppUrl: hs.Cfg.AppURL,
AppSubUrl: hs.Cfg.AppSubURL,
AllowOrgCreate: (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
@@ -143,7 +143,6 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
AngularSupportEnabled: hs.Cfg.AngularSupportEnabled,
EditorsCanAdmin: hs.Cfg.EditorsCanAdmin,
DisableSanitizeHtml: hs.Cfg.DisableSanitizeHtml,
PluginsToPreload: pluginsToPreload,
DateFormats: hs.Cfg.DateFormats,
Auth: dtos.FrontendSettingsAuthDTO{
@@ -259,7 +258,7 @@ func isSupportBundlesEnabled(hs *HTTPServer) bool {
hs.Features.IsEnabled(featuremgmt.FlagSupportBundles)
}
func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, enabledPlugins EnabledPlugins) (map[string]plugins.DataSourceDTO, error) {
func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlugins AvailablePlugins) (map[string]plugins.DataSourceDTO, error) {
orgDataSources := make([]*datasources.DataSource, 0)
if c.OrgID != 0 {
query := datasources.GetDataSourcesQuery{OrgID: c.OrgID, DataSourceLimit: hs.Cfg.DataSourceLimit}
@@ -300,11 +299,12 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, enabledPlugin
ReadOnly: ds.ReadOnly,
}
plugin, exists := enabledPlugins.Get(plugins.DataSource, ds.Type)
ap, exists := availablePlugins.Get(plugins.DataSource, ds.Type)
if !exists {
c.Logger.Error("Could not find plugin definition for data source", "datasource_type", ds.Type)
continue
}
plugin := ap.Plugin
dsDTO.Preload = plugin.Preload
dsDTO.Module = plugin.Module
dsDTO.PluginMeta = &plugins.PluginMetaDTO{
@@ -397,6 +397,21 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, enabledPlugin
return dataSources, nil
}
func newAppDTO(plugin plugins.PluginDTO, settings pluginsettings.InfoDTO) *plugins.AppDTO {
app := &plugins.AppDTO{
ID: plugin.ID,
Version: plugin.Info.Version,
Path: plugin.Module,
Preload: plugin.Preload,
}
if settings.Enabled {
app.Extensions = plugin.Extensions
}
return app
}
func getPanelSort(id string) int {
sort := 100
switch id {
@@ -438,52 +453,66 @@ func getPanelSort(id string) int {
return sort
}
// EnabledPlugins represents a mapping from plugin types (panel, data source, etc.) to plugin IDs to plugins
// For example ["panel"] -> ["piechart"] -> {pie chart plugin DTO}
type EnabledPlugins map[plugins.Type]map[string]plugins.PluginDTO
func (ep EnabledPlugins) Get(pluginType plugins.Type, pluginID string) (plugins.PluginDTO, bool) {
if _, exists := ep[pluginType][pluginID]; exists {
return ep[pluginType][pluginID], true
}
return plugins.PluginDTO{}, false
type availablePluginDTO struct {
Plugin plugins.PluginDTO
Settings pluginsettings.InfoDTO
}
func (hs *HTTPServer) enabledPlugins(ctx context.Context, orgID int64) (EnabledPlugins, error) {
ep := make(EnabledPlugins)
// AvailablePlugins represents a mapping from plugin types (panel, data source, etc.) to plugin IDs to plugins
// For example ["panel"] -> ["piechart"] -> {pie chart plugin DTO}
type AvailablePlugins map[plugins.Type]map[string]*availablePluginDTO
func (ap AvailablePlugins) Get(pluginType plugins.Type, pluginID string) (*availablePluginDTO, bool) {
if _, exists := ap[pluginType][pluginID]; exists {
return ap[pluginType][pluginID], true
}
return nil, false
}
func (hs *HTTPServer) availablePlugins(ctx context.Context, orgID int64) (AvailablePlugins, error) {
ap := make(AvailablePlugins)
pluginSettingMap, err := hs.pluginSettings(ctx, orgID)
if err != nil {
return ep, err
return ap, err
}
apps := make(map[string]plugins.PluginDTO)
apps := make(map[string]*availablePluginDTO)
for _, app := range hs.pluginStore.Plugins(ctx, plugins.App) {
if b, exists := pluginSettingMap[app.ID]; exists {
app.Pinned = b.Pinned
apps[app.ID] = app
if s, exists := pluginSettingMap[app.ID]; exists {
app.Pinned = s.Pinned
apps[app.ID] = &availablePluginDTO{
Plugin: app,
Settings: *s,
}
}
}
ep[plugins.App] = apps
ap[plugins.App] = apps
dataSources := make(map[string]plugins.PluginDTO)
dataSources := make(map[string]*availablePluginDTO)
for _, ds := range hs.pluginStore.Plugins(ctx, plugins.DataSource) {
if _, exists := pluginSettingMap[ds.ID]; exists {
dataSources[ds.ID] = ds
if s, exists := pluginSettingMap[ds.ID]; exists {
dataSources[ds.ID] = &availablePluginDTO{
Plugin: ds,
Settings: *s,
}
}
}
ep[plugins.DataSource] = dataSources
ap[plugins.DataSource] = dataSources
panels := make(map[string]plugins.PluginDTO)
panels := make(map[string]*availablePluginDTO)
for _, p := range hs.pluginStore.Plugins(ctx, plugins.Panel) {
if _, exists := pluginSettingMap[p.ID]; exists {
panels[p.ID] = p
if s, exists := pluginSettingMap[p.ID]; exists {
panels[p.ID] = &availablePluginDTO{
Plugin: p,
Settings: *s,
}
}
}
ep[plugins.Panel] = panels
ap[plugins.Panel] = panels
return ep, nil
return ap, nil
}
func (hs *HTTPServer) pluginSettings(ctx context.Context, orgID int64) (map[string]*pluginsettings.InfoDTO, error) {
+169 -11
View File
@@ -2,6 +2,7 @@ package api
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"path/filepath"
@@ -15,20 +16,19 @@ import (
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/plugindef"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing"
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service"
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/services/updatechecker"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*web.Mux, *HTTPServer) {
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.FeatureManager, pstore plugins.Store, psettings pluginSettings.Service) (*web.Mux, *HTTPServer) {
t.Helper()
db.InitTestDB(t)
cfg.IsFeatureToggleEnabled = features.IsEnabled
@@ -44,8 +44,15 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.
})
}
sqlStore := db.InitTestDB(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
var pluginStore = pstore
if pluginStore == nil {
pluginStore = &plugins.FakePluginStore{}
}
var pluginsSettings = psettings
if pluginsSettings == nil {
pluginsSettings = &pluginSettings.FakePluginSettings{}
}
hs := &HTTPServer{
Cfg: cfg,
@@ -55,12 +62,12 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.
Cfg: cfg,
RendererPluginManager: &fakeRendererManager{},
},
SQLStore: sqlStore,
SQLStore: db.InitTestDB(t),
SettingsProvider: setting.ProvideProvider(cfg),
pluginStore: &plugins.FakePluginStore{},
pluginStore: pluginStore,
grafanaUpdateChecker: &updatechecker.GrafanaService{},
AccessControl: accesscontrolmock.New().WithDisabled(),
PluginSettings: pluginSettings.ProvideService(sqlStore, secretsService),
PluginSettings: pluginsSettings,
pluginsCDNService: pluginscdn.ProvideService(&config.Cfg{
PluginsCDNURLTemplate: cfg.PluginsCDNURLTemplate,
PluginSettings: cfg.PluginSettings,
@@ -91,7 +98,7 @@ func TestHTTPServer_GetFrontendSettings_hideVersionAnonymous(t *testing.T) {
cfg.BuildVersion = "7.8.9"
cfg.BuildCommit = "01234567"
m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures())
m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), nil, nil)
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
@@ -182,7 +189,7 @@ func TestHTTPServer_GetFrontendSettings_pluginsCDNBaseURL(t *testing.T) {
if test.mutateCfg != nil {
test.mutateCfg(cfg)
}
m, _ := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures())
m, _ := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), nil, nil)
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
recorder := httptest.NewRecorder()
@@ -195,3 +202,154 @@ func TestHTTPServer_GetFrontendSettings_pluginsCDNBaseURL(t *testing.T) {
})
}
}
func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) {
type settings struct {
Apps map[string]*plugins.AppDTO `json:"apps"`
}
tests := []struct {
desc string
pluginStore func() plugins.Store
pluginSettings func() pluginSettings.Service
expected settings
}{
{
desc: "app without extensions",
pluginStore: func() plugins.Store {
return &plugins.FakePluginStore{
PluginList: newPlugins("test-app", nil),
}
},
pluginSettings: func() pluginSettings.Service {
return &pluginSettings.FakePluginSettings{
Plugins: newAppSettings("test-app", true),
}
},
expected: settings{
Apps: map[string]*plugins.AppDTO{
"test-app": {
ID: "test-app",
Preload: false,
Path: "/test-app/module.js",
Version: "0.5.0",
Extensions: nil,
},
},
},
},
{
desc: "enabled app with link extensions",
pluginStore: func() plugins.Store {
return &plugins.FakePluginStore{
PluginList: newPlugins("test-app", []*plugindef.ExtensionsLink{
{
Target: "core/home/menu",
Type: plugindef.ExtensionsLinkTypeLink,
Title: "Title",
Description: "Home route of app",
Path: "/home",
},
}),
}
},
pluginSettings: func() pluginSettings.Service {
return &pluginSettings.FakePluginSettings{
Plugins: newAppSettings("test-app", true),
}
},
expected: settings{
Apps: map[string]*plugins.AppDTO{
"test-app": {
ID: "test-app",
Preload: false,
Path: "/test-app/module.js",
Version: "0.5.0",
Extensions: []*plugindef.ExtensionsLink{
{
Target: "core/home/menu",
Type: plugindef.ExtensionsLinkTypeLink,
Title: "Title",
Description: "Home route of app",
Path: "/home",
},
},
},
},
},
},
{
desc: "disabled app with link extensions",
pluginStore: func() plugins.Store {
return &plugins.FakePluginStore{
PluginList: newPlugins("test-app", []*plugindef.ExtensionsLink{
{
Target: "core/home/menu",
Type: plugindef.ExtensionsLinkTypeLink,
Title: "Title",
Description: "Home route of app",
Path: "/home",
},
}),
}
},
pluginSettings: func() pluginSettings.Service {
return &pluginSettings.FakePluginSettings{
Plugins: newAppSettings("test-app", false),
}
},
expected: settings{
Apps: map[string]*plugins.AppDTO{
"test-app": {
ID: "test-app",
Preload: false,
Path: "/test-app/module.js",
Version: "0.5.0",
Extensions: nil,
},
},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
cfg := setting.NewCfg()
m, _ := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), test.pluginStore(), test.pluginSettings())
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
recorder := httptest.NewRecorder()
m.ServeHTTP(recorder, req)
var got settings
err := json.Unmarshal(recorder.Body.Bytes(), &got)
require.NoError(t, err)
require.Equal(t, http.StatusOK, recorder.Code)
require.EqualValues(t, test.expected, got)
})
}
}
func newAppSettings(id string, enabled bool) map[string]*pluginSettings.DTO {
return map[string]*pluginSettings.DTO{
id: {
ID: 0,
OrgID: 1,
PluginID: id,
Enabled: enabled,
},
}
}
func newPlugins(id string, extensions []*plugindef.ExtensionsLink) []plugins.PluginDTO {
return []plugins.PluginDTO{
{
Module: fmt.Sprintf("/%s/module.js", id),
JSONData: plugins.JSONData{
ID: id,
Info: plugins.Info{Version: "0.5.0"},
Type: plugins.App,
Extensions: extensions,
},
},
}
}
+6
View File
@@ -334,6 +334,12 @@ func (l *Loader) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error)
}
}
for i, extension := range plugin.Extensions {
if !filepath.IsAbs(extension.Path) {
plugin.Extensions[i].Path = path.Join("/", extension.Path)
}
}
return plugin, nil
}
+66
View File
@@ -8,6 +8,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/plugindef"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/google/go-cmp/cmp"
@@ -462,7 +463,72 @@ func TestLoader_Load(t *testing.T) {
},
},
},
{
name: "Load an app with link extensions",
class: plugins.External,
cfg: &config.Cfg{
PluginsAllowUnsigned: []string{"test-app"},
},
pluginPaths: []string{"../testdata/test-app-with-link-extensions"},
want: []*plugins.Plugin{
{JSONData: plugins.JSONData{
ID: "test-app",
Type: "app",
Name: "Test App",
Info: plugins.Info{
Author: plugins.InfoLink{
Name: "Test Inc.",
URL: "http://test.com",
},
Description: "Official Grafana Test App & Dashboard bundle",
Version: "1.0.0",
Links: []plugins.InfoLink{
{Name: "Project site", URL: "http://project.com"},
{Name: "License & Terms", URL: "http://license.com"},
},
Logos: plugins.Logos{
Small: "public/img/icn-app.svg",
Large: "public/img/icn-app.svg",
},
Updated: "2015-02-10",
},
Dependencies: plugins.Dependencies{
GrafanaDependency: ">=8.0.0",
GrafanaVersion: "*",
Plugins: []plugins.Dependency{},
},
Includes: []*plugins.Includes{
{Name: "Root Page (react)", Type: "page", Role: "Viewer", Path: "/a/my-simple-app", DefaultNav: true, AddToNav: true, Slug: "root-page-react"},
},
Extensions: []*plugindef.ExtensionsLink{
{
Target: "plugins/grafana-slo-app/slo-breach",
Title: "Declare incident",
Type: plugindef.ExtensionsLinkTypeLink,
Description: "Declares a new incident",
Path: "/incidents/declare",
},
{
Target: "plugins/grafana-slo-app/slo-breach",
Title: "Declare incident",
Type: plugindef.ExtensionsLinkTypeLink,
Description: "Declares a new incident (path without backslash)",
Path: "/incidents/declare",
},
},
Backend: false,
},
DefaultNavURL: "/plugins/test-app/page/root-page-react",
PluginDir: filepath.Join(parentDir, "testdata/test-app-with-link-extensions"),
Class: plugins.External,
Signature: plugins.SignatureUnsigned,
Module: "plugins/test-app/module",
BaseURL: "public/plugins/test-app",
},
},
},
}
for _, tt := range tests {
reg := fakes.NewFakePluginRegistry()
storage := fakes.NewFakePluginStorage()
@@ -0,0 +1,56 @@
{
"type": "app",
"name": "Test App",
"id": "test-app",
"info": {
"description": "Official Grafana Test App & Dashboard bundle",
"author": {
"name": "Test Inc.",
"url": "http://test.com"
},
"keywords": [
"test"
],
"links": [
{
"name": "Project site",
"url": "http://project.com"
},
{
"name": "License & Terms",
"url": "http://license.com"
}
],
"version": "1.0.0",
"updated": "2015-02-10"
},
"includes": [
{
"type": "page",
"name": "Root Page (react)",
"path": "/a/my-simple-app",
"role": "Viewer",
"addToNav": true,
"defaultNav": true
}
],
"extensions": [
{
"target": "plugins/grafana-slo-app/slo-breach",
"type": "link",
"title": "Declare incident",
"description": "Declares a new incident",
"path": "/incidents/declare"
},
{
"target": "plugins/grafana-slo-app/slo-breach",
"type": "link",
"title": "Declare incident",
"description": "Declares a new incident (path without backslash)",
"path": "incidents/declare"
}
],
"dependencies": {
"grafanaDependency": ">=8.0.0"
}
}
+9 -5
View File
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"github.com/grafana/grafana/pkg/plugins/plugindef"
"github.com/grafana/grafana/pkg/services/org"
)
@@ -253,6 +254,14 @@ type PanelDTO struct {
Module string `json:"module"`
}
type AppDTO struct {
ID string `json:"id"`
Path string `json:"path"`
Version string `json:"version"`
Preload bool `json:"preload"`
Extensions []*plugindef.ExtensionsLink `json:"extensions,omitempty"`
}
const (
signatureMissing ErrorCode = "signatureMissing"
signatureModified ErrorCode = "signatureModified"
@@ -266,11 +275,6 @@ type Error struct {
PluginID string `json:"pluginId,omitempty"`
}
type PreloadPlugin struct {
Path string `json:"path"`
Version string `json:"version"`
}
// Access-Control related definitions
// RoleRegistration stores a role and its assignments to basic roles
+4
View File
@@ -84,6 +84,10 @@ func TestParsePluginTestdata(t *testing.T) {
rootid: "test-app",
skip: "has a 'page'-type include which isn't a known part of spec",
},
"test-app-with-link-extensions": {
rootid: "test-app",
skip: "has a 'page'-type include which isn't a known part of spec",
},
"test-app-with-roles": {
rootid: "test-app",
},
+22 -5
View File
@@ -1,9 +1,9 @@
package plugindef
import (
"strings"
"regexp"
"strings"
"github.com/grafana/thema"
)
@@ -122,6 +122,23 @@ seqs: [
...
}
#ExtensionsLink: {
// Target where the link will be rendered
target: =~"^(plugins|grafana)\/[a-z-/0-9]*$"
// Type of extension
type: "link"
// Title that will be displayed for the rendered link
title: string & strings.MinRunes(3) & strings.MaxRunes(22)
// Description for the rendered link
description: string & strings.MaxRunes(200)
// Path relative to the extending plugin e.g. /incidents/declare
path: =~"^\/.*"
...
}
// Extensions made by the current plugin.
extensions?: [...#ExtensionsLink]
// For data source plugins, if the plugin supports logs.
logs?: bool
@@ -175,9 +192,9 @@ seqs: [
// each of which has an action and an optional scope.
// Example: the role 'Schedules Reader' bundles permissions to view all schedules of the plugin.
#Role: {
name: string,
name: =~"^([A-Z][0-9A-Za-z ]+)$"
description: string,
name: string
name: =~"^([A-Z][0-9A-Za-z ]+)$"
description: string
permissions: [...#Permission]
}
@@ -29,6 +29,11 @@ const (
DependencyTypePanel DependencyType = "panel"
)
// Defines values for ExtensionsLinkType.
const (
ExtensionsLinkTypeLink ExtensionsLinkType = "link"
)
// Defines values for IncludeRole.
const (
IncludeRoleAdmin IncludeRole = "Admin"
@@ -148,6 +153,27 @@ type Dependency struct {
// DependencyType defines model for Dependency.Type.
type DependencyType string
// ExtensionsLink defines model for ExtensionsLink.
type ExtensionsLink struct {
// Description for the rendered link
Description string `json:"description"`
// Path relative to the extending plugin e.g. /incidents/declare
Path string `json:"path"`
// Target where the link will be rendered
Target string `json:"target"`
// Title that will be displayed for the rendered link
Title string `json:"title"`
// Type of extension
Type ExtensionsLinkType `json:"type"`
}
// Type of extension
type ExtensionsLinkType string
// Header describes an HTTP header that is forwarded with a proxied request for
// a plugin route.
type Header struct {
@@ -314,6 +340,9 @@ type PluginDef struct {
// https://golang.org/doc/install/source#environment.
Executable *string `json:"executable,omitempty"`
// Extensions made by the current plugin.
Extensions *[]ExtensionsLink `json:"extensions,omitempty"`
// For data source plugins, include hidden queries in the data
// request.
HiddenQueries *bool `json:"hiddenQueries,omitempty"`
+3 -1
View File
@@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
"github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
"github.com/grafana/grafana/pkg/plugins/plugindef"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/util"
)
@@ -145,7 +146,8 @@ type JSONData struct {
SkipDataQuery bool `json:"skipDataQuery"`
// App settings
AutoEnabled bool `json:"autoEnabled"`
AutoEnabled bool `json:"autoEnabled"`
Extensions []*plugindef.ExtensionsLink `json:"extensions"`
// Datasource settings
Annotations bool `json:"annotations"`