diff --git a/pkg/api/api.go b/pkg/api/api.go index f616efb59ed..dc26a6f4601 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -13,7 +13,7 @@ func Register(r *macaron.Macaron) { reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true}) reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true}) reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN) - regOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN) + reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN) quota := middleware.Quota bind := binding.Bind @@ -113,7 +113,7 @@ func Register(r *macaron.Macaron) { r.Get("/invites", wrap(GetPendingOrgInvites)) r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite)) r.Patch("/invites/:code/revoke", wrap(RevokeInvite)) - }, regOrgAdmin) + }, reqOrgAdmin) // create new org r.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg)) @@ -140,7 +140,7 @@ func Register(r *macaron.Macaron) { r.Get("/", wrap(GetApiKeys)) r.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddApiKey)) r.Delete("/:id", wrap(DeleteApiKey)) - }, regOrgAdmin) + }, reqOrgAdmin) // Data sources r.Group("/datasources", func() { @@ -150,7 +150,13 @@ func Register(r *macaron.Macaron) { r.Delete("/:id", DeleteDataSource) r.Get("/:id", wrap(GetDataSourceById)) r.Get("/plugins", GetDataSourcePlugins) - }, regOrgAdmin) + }, reqOrgAdmin) + + // PluginBundles + r.Group("/plugins", func() { + r.Get("/", wrap(GetPluginBundles)) + r.Post("/", bind(m.UpdatePluginBundleCmd{}), wrap(UpdatePluginBundle)) + }, reqOrgAdmin) r.Get("/frontend/settings/", GetFrontendSettings) r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index b2c554de0e2..c6081af50ad 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -115,9 +115,13 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) { func GetDataSourcePlugins(c *middleware.Context) { dsList := make(map[string]interface{}) - //TODO(awoods): query DB for orgPlugins - orgPlugins := map[string]m.PluginBundle{} - enabledPlugins := plugins.GetEnabledPlugins(orgPlugins) + + orgBundles := m.GetPluginBundlesQuery{OrgId: c.OrgId} + err := bus.Dispatch(&orgBundles) + if err != nil { + c.JsonApiErr(500, "Failed to get org plugin Bundles", err) + } + enabledPlugins := plugins.GetEnabledPlugins(orgBundles.Result) for key, value := range enabledPlugins.DataSourcePlugins { if !value.BuiltIn { diff --git a/pkg/api/dtos/plugin_bundle.go b/pkg/api/dtos/plugin_bundle.go new file mode 100644 index 00000000000..f043da39904 --- /dev/null +++ b/pkg/api/dtos/plugin_bundle.go @@ -0,0 +1,8 @@ +package dtos + +type PluginBundle struct { + Type string `json:"type"` + Enabled bool `json:"enabled"` + Module string `json:"module"` + JsonData map[string]interface{} `json:"jsonData"` +} diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 49b679af194..a6b27327dfd 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -29,9 +29,12 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro datasources := make(map[string]interface{}) var defaultDatasource string - //TODO(awoods): query DB to get list of the users plugin preferences. - orgPlugins := map[string]m.PluginBundle{} - enabledPlugins := plugins.GetEnabledPlugins(orgPlugins) + orgBundles := m.GetPluginBundlesQuery{OrgId: c.OrgId} + err := bus.Dispatch(&orgBundles) + if err != nil { + return nil, err + } + enabledPlugins := plugins.GetEnabledPlugins(orgBundles.Result) for _, ds := range orgDataSources { url := ds.Url diff --git a/pkg/api/index.go b/pkg/api/index.go index b9f880faefc..357aa7d335c 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -2,6 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" @@ -62,9 +63,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { }) } - //TODO(awoods): query DB to get list of the users plugin preferences. - orgPlugins := map[string]m.PluginBundle{} - enabledPlugins := plugins.GetEnabledPlugins(orgPlugins) + orgBundles := m.GetPluginBundlesQuery{OrgId: c.OrgId} + err = bus.Dispatch(&orgBundles) + if err != nil { + return nil, err + } + enabledPlugins := plugins.GetEnabledPlugins(orgBundles.Result) + for _, plugin := range enabledPlugins.ExternalPlugins { for _, js := range plugin.Js { data.PluginJs = append(data.PluginJs, js.Module) diff --git a/pkg/api/plugin_bundle.go b/pkg/api/plugin_bundle.go new file mode 100644 index 00000000000..9378189af7d --- /dev/null +++ b/pkg/api/plugin_bundle.go @@ -0,0 +1,65 @@ +package api + +import ( + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/middleware" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" +) + +func GetPluginBundles(c *middleware.Context) Response { + query := m.GetPluginBundlesQuery{OrgId: c.OrgId} + + if err := bus.Dispatch(&query); err != nil { + return ApiError(500, "Failed to list Plugin Bundles", err) + } + + installedBundlesMap := make(map[string]*dtos.PluginBundle) + for t, b := range plugins.Bundles { + installedBundlesMap[t] = &dtos.PluginBundle{ + Type: b.Type, + Enabled: b.Enabled, + Module: b.Module, + JsonData: make(map[string]interface{}), + } + } + + seenBundles := make(map[string]bool) + + result := make([]*dtos.PluginBundle, 0) + for _, b := range query.Result { + if def, ok := installedBundlesMap[b.Type]; ok { + result = append(result, &dtos.PluginBundle{ + Type: b.Type, + Enabled: b.Enabled, + Module: def.Module, + JsonData: b.JsonData, + }) + seenBundles[b.Type] = true + } + } + + for t, b := range installedBundlesMap { + if _, ok := seenBundles[t]; !ok { + result = append(result, b) + } + } + + return Json(200, result) +} + +func UpdatePluginBundle(c *middleware.Context, cmd m.UpdatePluginBundleCmd) Response { + cmd.OrgId = c.OrgId + + if _, ok := plugins.Bundles[cmd.Type]; !ok { + return ApiError(404, "Bundle type not installed.", nil) + } + + err := bus.Dispatch(&cmd) + if err != nil { + return ApiError(500, "Failed to update plugin bundle", err) + } + + return ApiSuccess("Plugin updated") +} diff --git a/pkg/models/plugin_bundle.go b/pkg/models/plugin_bundle.go index 3c126a913ac..5f4e508b9b2 100644 --- a/pkg/models/plugin_bundle.go +++ b/pkg/models/plugin_bundle.go @@ -1,8 +1,34 @@ package models +import "time" + type PluginBundle struct { - Id int64 - Type string - Org int64 - Enabled bool + Id int64 + Type string + OrgId int64 + Enabled bool + JsonData map[string]interface{} + + Created time.Time + Updated time.Time +} + +// ---------------------- +// COMMANDS + +// Also acts as api DTO +type UpdatePluginBundleCmd struct { + Type string `json:"type" binding:"Required"` + Enabled bool `json:"enabled"` + JsonData map[string]interface{} `json:"jsonData"` + + Id int64 `json:"-"` + OrgId int64 `json:"-"` +} + +// --------------------- +// QUERIES +type GetPluginBundlesQuery struct { + OrgId int64 + Result []*PluginBundle } diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 255ea462e67..05b9492f4e5 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -37,8 +37,7 @@ type ExternalPluginRoute struct { } type ExternalPluginJs struct { - Module string `json:"module"` - Directive string `json:"Directive"` + Module string `json:"module"` } type ExternalPluginNavLink struct { @@ -68,6 +67,7 @@ type PluginBundle struct { PanelPlugins []string `json:"panelPlugins"` DatasourcePlugins []string `json:"datasourcePlugins"` ExternalPlugins []string `json:"externalPlugins"` + Module string `json:"module"` } type EnabledPlugins struct { diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 6d45267f8d8..935e3bf9c21 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -172,13 +172,18 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error { return nil } -func GetEnabledPlugins(bundles map[string]models.PluginBundle) EnabledPlugins { +func GetEnabledPlugins(orgBundles []*models.PluginBundle) EnabledPlugins { enabledPlugins := NewEnabledPlugins() + orgBundlesMap := make(map[string]*models.PluginBundle) + for _, orgBundle := range orgBundles { + orgBundlesMap[orgBundle.Type] = orgBundle + } + for bundleType, bundle := range Bundles { enabled := bundle.Enabled // check if the bundle is stored in the DB. - if b, ok := bundles[bundleType]; ok { + if b, ok := orgBundlesMap[bundleType]; ok { enabled = b.Enabled } diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 8f7054d3959..569d26282ed 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -18,6 +18,7 @@ func AddMigrations(mg *Migrator) { addApiKeyMigrations(mg) addDashboardSnapshotMigrations(mg) addQuotaMigration(mg) + addPluginBundleMigration(mg) } func addMigrationLogMigrations(mg *Migrator) { diff --git a/pkg/services/sqlstore/migrations/plugin_bundle.go b/pkg/services/sqlstore/migrations/plugin_bundle.go new file mode 100644 index 00000000000..b56ea74a13e --- /dev/null +++ b/pkg/services/sqlstore/migrations/plugin_bundle.go @@ -0,0 +1,26 @@ +package migrations + +import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +func addPluginBundleMigration(mg *Migrator) { + + var pluginBundleV1 = Table{ + Name: "plugin_bundle", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "org_id", Type: DB_BigInt, Nullable: true}, + {Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "enabled", Type: DB_Bool, Nullable: false}, + {Name: "json_data", Type: DB_Text, Nullable: true}, + {Name: "created", Type: DB_DateTime, Nullable: false}, + {Name: "updated", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"org_id", "type"}, Type: UniqueIndex}, + }, + } + mg.AddMigration("create plugin_bundle table v1", NewAddTableMigration(pluginBundleV1)) + + //------- indexes ------------------ + addTableIndicesMigrations(mg, "v1", pluginBundleV1) +} diff --git a/pkg/services/sqlstore/plugin_bundle.go b/pkg/services/sqlstore/plugin_bundle.go new file mode 100644 index 00000000000..c15c263a100 --- /dev/null +++ b/pkg/services/sqlstore/plugin_bundle.go @@ -0,0 +1,46 @@ +package sqlstore + +import ( + "time" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +func init() { + bus.AddHandler("sql", GetPluginBundles) + bus.AddHandler("sql", UpdatePluginBundle) +} + +func GetPluginBundles(query *m.GetPluginBundlesQuery) error { + sess := x.Where("org_id=?", query.OrgId) + + query.Result = make([]*m.PluginBundle, 0) + return sess.Find(&query.Result) +} + +func UpdatePluginBundle(cmd *m.UpdatePluginBundleCmd) error { + return inTransaction2(func(sess *session) error { + var bundle m.PluginBundle + + exists, err := sess.Where("org_id=? and type=?", cmd.OrgId, cmd.Type).Get(&bundle) + sess.UseBool("enabled") + if !exists { + bundle = m.PluginBundle{ + Type: cmd.Type, + OrgId: cmd.OrgId, + Enabled: cmd.Enabled, + JsonData: cmd.JsonData, + Created: time.Now(), + Updated: time.Now(), + } + _, err = sess.Insert(&bundle) + return err + } else { + bundle.Enabled = cmd.Enabled + bundle.JsonData = cmd.JsonData + _, err = sess.Id(bundle.Id).Update(&bundle) + return err + } + }) +} diff --git a/public/app/plugins/plugin.json b/public/app/plugins/plugin.json index bbb637bcf9c..d7356c7b8ad 100644 --- a/public/app/plugins/plugin.json +++ b/public/app/plugins/plugin.json @@ -1,8 +1,9 @@ { "pluginType": "bundle", "type": "core", + "module": "", "enabled": true, - "panelPlugins": ["graph", "singlestat", "text", "dashlist"], - "datasourcePlugins": ["grafana", "graphite"], + "panelPlugins": ["graph", "singlestat", "text", "dashlist", "table"], + "datasourcePlugins": ["mixed", "grafana", "graphite", "cloudwatch", "elasticsearch", "influxdb", "influxdb_08", "kairosdb", "opentsdb", "prometheus"], "externalPlugins": [] }