mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 10:22:30 +08:00
feat(apps): more work on apps and how apps can include panels
This commit is contained in:
@ -12,17 +12,19 @@ type AppSettings struct {
|
|||||||
Pinned bool `json:"pinned"`
|
Pinned bool `json:"pinned"`
|
||||||
Module string `json:"module"`
|
Module string `json:"module"`
|
||||||
Info *plugins.PluginInfo `json:"info"`
|
Info *plugins.PluginInfo `json:"info"`
|
||||||
Pages []*plugins.AppPluginPage `json:"pages"`
|
Pages []plugins.AppPluginPage `json:"pages"`
|
||||||
|
Includes []plugins.AppIncludeInfo `json:"includes"`
|
||||||
JsonData map[string]interface{} `json:"jsonData"`
|
JsonData map[string]interface{} `json:"jsonData"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAppSettingsDto(def *plugins.AppPlugin, data *models.AppSettings) *AppSettings {
|
func NewAppSettingsDto(def *plugins.AppPlugin, data *models.AppSettings) *AppSettings {
|
||||||
dto := &AppSettings{
|
dto := &AppSettings{
|
||||||
AppId: def.Id,
|
AppId: def.Id,
|
||||||
Name: def.Name,
|
Name: def.Name,
|
||||||
Info: &def.Info,
|
Info: &def.Info,
|
||||||
Module: def.Module,
|
Module: def.Module,
|
||||||
Pages: def.Pages,
|
Pages: def.Pages,
|
||||||
|
Includes: def.Includes,
|
||||||
}
|
}
|
||||||
|
|
||||||
if data != nil {
|
if data != nil {
|
||||||
|
@ -2,6 +2,7 @@ package plugins
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
)
|
)
|
||||||
@ -17,10 +18,17 @@ type AppPluginCss struct {
|
|||||||
Dark string `json:"dark"`
|
Dark string `json:"dark"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AppIncludeInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
type AppPlugin struct {
|
type AppPlugin struct {
|
||||||
FrontendPluginBase
|
FrontendPluginBase
|
||||||
Css *AppPluginCss `json:"css"`
|
Css *AppPluginCss `json:"css"`
|
||||||
Pages []*AppPluginPage `json:"pages"`
|
Pages []AppPluginPage `json:"pages"`
|
||||||
|
Includes []AppIncludeInfo `json:"-"`
|
||||||
|
|
||||||
Pinned bool `json:"-"`
|
Pinned bool `json:"-"`
|
||||||
Enabled bool `json:"-"`
|
Enabled bool `json:"-"`
|
||||||
@ -38,6 +46,19 @@ func (app *AppPlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
|||||||
|
|
||||||
app.PluginDir = pluginDir
|
app.PluginDir = pluginDir
|
||||||
app.initFrontendPlugin()
|
app.initFrontendPlugin()
|
||||||
|
|
||||||
|
// check if we have child panels
|
||||||
|
for _, panel := range Panels {
|
||||||
|
if strings.HasPrefix(panel.PluginDir, app.PluginDir) {
|
||||||
|
panel.IncludedInAppId = app.Id
|
||||||
|
app.Includes = append(app.Includes, AppIncludeInfo{
|
||||||
|
Name: panel.Name,
|
||||||
|
Id: panel.Id,
|
||||||
|
Type: panel.Type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Apps[app.Id] = app
|
Apps[app.Id] = app
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -8,21 +8,22 @@ import (
|
|||||||
|
|
||||||
type FrontendPluginBase struct {
|
type FrontendPluginBase struct {
|
||||||
PluginBase
|
PluginBase
|
||||||
Module string `json:"module"`
|
Module string `json:"module"`
|
||||||
StaticRoot string `json:"staticRoot"`
|
StaticRoot string `json:"staticRoot"`
|
||||||
|
StaticRootAbs string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fp *FrontendPluginBase) initFrontendPlugin() {
|
func (fp *FrontendPluginBase) initFrontendPlugin() {
|
||||||
if fp.StaticRoot != "" {
|
if fp.StaticRoot != "" {
|
||||||
|
fp.StaticRootAbs = filepath.Join(fp.PluginDir, fp.StaticRoot)
|
||||||
StaticRoutes = append(StaticRoutes, &PluginStaticRoute{
|
StaticRoutes = append(StaticRoutes, &PluginStaticRoute{
|
||||||
Directory: filepath.Join(fp.PluginDir, fp.StaticRoot),
|
Directory: fp.StaticRootAbs,
|
||||||
PluginId: fp.Id,
|
PluginId: fp.Id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fp.Info.Logos.Small = evalRelativePluginUrlPath(fp.Info.Logos.Small, fp.Id)
|
fp.Info.Logos.Small = evalRelativePluginUrlPath(fp.Info.Logos.Small, fp.Id)
|
||||||
fp.Info.Logos.Large = evalRelativePluginUrlPath(fp.Info.Logos.Large, fp.Id)
|
fp.Info.Logos.Large = evalRelativePluginUrlPath(fp.Info.Logos.Large, fp.Id)
|
||||||
|
|
||||||
fp.handleModuleDefaults()
|
fp.handleModuleDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,12 +11,13 @@ type PluginLoader interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PluginBase struct {
|
type PluginBase struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
App string `json:"app"`
|
Info PluginInfo `json:"info"`
|
||||||
Info PluginInfo `json:"info"`
|
|
||||||
PluginDir string `json:"-"`
|
IncludedInAppId string `json:"-"`
|
||||||
|
PluginDir string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginInfo struct {
|
type PluginInfo struct {
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
package plugins
|
package plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@ -122,28 +119,6 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func interpolatePluginJson(reader io.Reader, pluginCommon *PluginBase) (io.Reader, error) {
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
buf.ReadFrom(reader)
|
|
||||||
jsonStr := buf.String() //
|
|
||||||
|
|
||||||
tmpl, err := template.New("json").Parse(jsonStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"PluginPublicRoot": "public/plugins/" + pluginCommon.Id,
|
|
||||||
}
|
|
||||||
|
|
||||||
var resultBuffer bytes.Buffer
|
|
||||||
if err := tmpl.ExecuteTemplate(&resultBuffer, "json", data); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return bytes.NewReader(resultBuffer.Bytes()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
|
func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
|
||||||
currentDir := filepath.Dir(pluginJsonFilePath)
|
currentDir := filepath.Dir(pluginJsonFilePath)
|
||||||
reader, err := os.Open(pluginJsonFilePath)
|
reader, err := os.Open(pluginJsonFilePath)
|
||||||
@ -163,20 +138,13 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
|
|||||||
return errors.New("Did not find type and id property in plugin.json")
|
return errors.New("Did not find type and id property in plugin.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.Seek(0, 0)
|
|
||||||
if newReader, err := interpolatePluginJson(reader, &pluginCommon); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
jsonParser = json.NewDecoder(newReader)
|
|
||||||
}
|
|
||||||
|
|
||||||
var loader PluginLoader
|
var loader PluginLoader
|
||||||
|
|
||||||
if pluginGoType, exists := PluginTypes[pluginCommon.Type]; !exists {
|
if pluginGoType, exists := PluginTypes[pluginCommon.Type]; !exists {
|
||||||
return errors.New("Unkown plugin type " + pluginCommon.Type)
|
return errors.New("Unkown plugin type " + pluginCommon.Type)
|
||||||
} else {
|
} else {
|
||||||
loader = reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader)
|
loader = reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reader.Seek(0, 0)
|
||||||
return loader.Load(jsonParser, currentDir)
|
return loader.Load(jsonParser, currentDir)
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,7 @@ func GetEnabledPlugins(orgId int64) (*EnabledPlugins, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
seenPanels := make(map[string]bool)
|
enabledApps := make(map[string]bool)
|
||||||
seenApi := make(map[string]bool)
|
|
||||||
|
|
||||||
for appId, installedApp := range Apps {
|
for appId, installedApp := range Apps {
|
||||||
var app AppPlugin
|
var app AppPlugin
|
||||||
@ -42,32 +41,36 @@ func GetEnabledPlugins(orgId int64) (*EnabledPlugins, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if app.Enabled {
|
if app.Enabled {
|
||||||
|
enabledApps[app.Id] = true
|
||||||
enabledPlugins.Apps = append(enabledPlugins.Apps, &app)
|
enabledPlugins.Apps = append(enabledPlugins.Apps, &app)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isPluginEnabled := func(appId string) bool {
|
||||||
|
if appId == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := enabledApps[appId]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
// add all plugins that are not part of an App.
|
// add all plugins that are not part of an App.
|
||||||
for d, installedDs := range DataSources {
|
for dsId, ds := range DataSources {
|
||||||
if installedDs.App == "" {
|
if isPluginEnabled(ds.IncludedInAppId) {
|
||||||
enabledPlugins.DataSources[d] = installedDs
|
enabledPlugins.DataSources[dsId] = ds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for p, panel := range Panels {
|
for _, panel := range Panels {
|
||||||
if panel.App == "" {
|
if isPluginEnabled(panel.IncludedInAppId) {
|
||||||
if _, ok := seenPanels[p]; !ok {
|
enabledPlugins.Panels = append(enabledPlugins.Panels, panel)
|
||||||
seenPanels[p] = true
|
|
||||||
enabledPlugins.Panels = append(enabledPlugins.Panels, panel)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for a, api := range ApiPlugins {
|
for _, api := range ApiPlugins {
|
||||||
if api.App == "" {
|
if isPluginEnabled(api.IncludedInAppId) {
|
||||||
if _, ok := seenApi[a]; !ok {
|
enabledPlugins.ApiList = append(enabledPlugins.ApiList, api)
|
||||||
seenApi[a] = true
|
|
||||||
enabledPlugins.ApiList = append(enabledPlugins.ApiList, api)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import _ from 'lodash';
|
|||||||
|
|
||||||
export class AppEditCtrl {
|
export class AppEditCtrl {
|
||||||
appModel: any;
|
appModel: any;
|
||||||
|
includedPanels: any;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private backendSrv: any, private $routeParams: any) {
|
constructor(private backendSrv: any, private $routeParams: any) {
|
||||||
@ -12,6 +13,7 @@ export class AppEditCtrl {
|
|||||||
|
|
||||||
this.backendSrv.get(`/api/org/apps/${this.$routeParams.appId}/settings`).then(result => {
|
this.backendSrv.get(`/api/org/apps/${this.$routeParams.appId}/settings`).then(result => {
|
||||||
this.appModel = result;
|
this.appModel = result;
|
||||||
|
this.includedPanels = _.where(result.includes, {type: 'panel'});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,10 @@
|
|||||||
Panels
|
Panels
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li><em class="small">None</em></li>
|
<li ng-show="!ctrl.includedPanels.length"><em class="small">None</em></li>
|
||||||
|
<li ng-repeat="panel in ctrl.includedPanels">
|
||||||
|
{{panel.name}}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="simple-box-body simple-box-column">
|
<div class="simple-box-body simple-box-column">
|
||||||
|
@ -3,13 +3,21 @@
|
|||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
|
||||||
|
import {unknownPanelDirective} from '../../plugins/panel/unknown/module';
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
function panelLoader($parse, dynamicDirectiveSrv) {
|
function panelLoader($parse, dynamicDirectiveSrv) {
|
||||||
return dynamicDirectiveSrv.create({
|
return dynamicDirectiveSrv.create({
|
||||||
directive: scope => {
|
directive: scope => {
|
||||||
let modulePath = config.panels[scope.panel.type].module;
|
let panelInfo = config.panels[scope.panel.type];
|
||||||
|
if (!panelInfo) {
|
||||||
|
return Promise.resolve({
|
||||||
|
name: 'panel-directive-' + scope.panel.type,
|
||||||
|
fn: unknownPanelDirective
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return System.import(modulePath).then(function(panelModule) {
|
return System.import(panelInfo.module).then(function(panelModule) {
|
||||||
return {
|
return {
|
||||||
name: 'panel-directive-' + scope.panel.type,
|
name: 'panel-directive-' + scope.panel.type,
|
||||||
fn: panelModule.panel,
|
fn: panelModule.panel,
|
||||||
|
15
public/app/plugins/panel/unknown/module.ts
Normal file
15
public/app/plugins/panel/unknown/module.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
///<reference path="../../../headers/common.d.ts" />
|
||||||
|
|
||||||
|
export function unknownPanelDirective() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
template: `
|
||||||
|
<grafana-panel>
|
||||||
|
<div class="text-center" style="padding-top: 2rem">
|
||||||
|
Unknown panel type: <strong>{{panel.type}}</strong>
|
||||||
|
</div>
|
||||||
|
</grafana-panel>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
@simpleBoxBorderWidth: 0.2rem;
|
@simpleBoxBorderWidth: 0.2rem;
|
||||||
@simpleBoxMargin: 1.5rem;
|
@simpleBoxMargin: 1.5rem;
|
||||||
@simpleBoxBodyPadding: 0.5rem 0 0.5rem 1rem;
|
@simpleBoxBodyPadding: 1rem 0 0.5rem 1rem;
|
||||||
|
|
||||||
.simple-box {
|
.simple-box {
|
||||||
margin-top: @simpleBoxMargin;
|
margin-top: @simpleBoxMargin;
|
||||||
|
Reference in New Issue
Block a user