diff --git a/CHANGELOG.md b/CHANGELOG.md index 40a0421d45a..8a5f8b0d0d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * **KairosDB** The data source is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3524](https://github.com/grafana/grafana/issues/3524) ### Enhancements +* **LDAP**: Support for nested LDAP Groups, closes [#4401](https://github.com/grafana/grafana/issues/4401), [#3808](https://github.com/grafana/grafana/issues/3808) * **Sessions**: Support for memcached as session storage, closes [#3458](https://github.com/grafana/grafana/issues/3458) * **mysql**: Grafana now supports ssl for mysql, closes [#3584](https://github.com/grafana/grafana/issues/3584) * **snapshot**: Annotations are now included in snapshots, closes [#3635](https://github.com/grafana/grafana/issues/3635) diff --git a/docs/sources/plugins/app.md b/docs/sources/plugins/app.md new file mode 100644 index 00000000000..250af25c57d --- /dev/null +++ b/docs/sources/plugins/app.md @@ -0,0 +1,46 @@ +--- +page_title: App plugin +page_description: App plugin for Grafana +page_keywords: grafana, plugins, documentation +--- + + > Our goal is not to have a very extensive documentation but rather have actual code that people can look at. An example implementation of an app can be found in this [example app repo](https://github.com/grafana/example-app) + +# Apps + +App plugins is a new kind of grafana plugin that can bundle datasource and panel plugins within one package. It also enable the plugin author to create custom pages within grafana. The custom pages enables the plugin author to include things like documentation, sign up forms or controlling other services using HTTP requests. + +Datasource and panel plugins will show up like normal plugins. The app pages will be available in the main menu. + + + +## Enabling app plugins +After installing an app it have to be enabled before it show up as an datasource or panel. You can do that on the app page in the config tab. + +## README.md + +The readme file in the mounted folder will show up in the overview tab on the app page. + +## Module exports +```javascript +export { + ExampleAppConfigCtrl as ConfigCtrl, + StreamPageCtrl, + LogsPageCtrl +}; +``` +The only required export is the ConfigCtrl. Both StreamPageCtrl and LogsPageCtrl are custom pages defined in plugin.json + +## Custom pages +Custom pages are defined in the plugin.json like this. +```json +"pages": [ + { "name": "Live stream", "component": "StreamPageCtrl", "role": "Editor"}, + { "name": "Log view", "component": "LogsPageCtrl", "role": "Viewer"} +] +``` +The component field have to match one of the components exported in the module.js in the root of the plugin. + +## Bundled plugins + +When Grafana starts it will scan all directories within an app plugin and load folders containing a plugin.json as an plugin. diff --git a/docs/sources/plugins/datasources.md b/docs/sources/plugins/datasources.md index 6bd11242f52..c44cd842844 100644 --- a/docs/sources/plugins/datasources.md +++ b/docs/sources/plugins/datasources.md @@ -4,7 +4,7 @@ page_description: Datasource plugins for Grafana page_keywords: grafana, plugins, documentation --- - > Our goal is not to have a very extensive documentation but rather have actual code that people can look at. An example implementation of a datasource can be found in the grafana repo under /examples/datasource-plugin-genericdatasource + > Our goal is not to have a very extensive documentation but rather have actual code that people can look at. An example implementation of a datasource can be found in this [example datasource repo](https://github.com/grafana/simple-json-datasource) # Datasources diff --git a/docs/sources/plugins/developing_plugins.md b/docs/sources/plugins/developing_plugins.md index b235fa8d3de..91d1ff3f84e 100644 --- a/docs/sources/plugins/developing_plugins.md +++ b/docs/sources/plugins/developing_plugins.md @@ -22,9 +22,7 @@ The easiset way to try your plugin with grafana is to [setup grafana for develop ## Examples / boilerplate We currently have three different examples that you can fork to get started developing your grafana plugin. - - [generic-datasource](https://github.com/grafana/grafana/tree/master/examples/datasource-plugin-genericdatasource) (small datasource plugin for quering json data from backends) + - [simple-json-datasource](https://github.com/grafana/simple-json-datasource) (small datasource plugin for quering json data from backends) - [panel-boilderplate-es5](https://github.com/grafana/grafana/tree/master/examples/panel-boilerplate-es5) - - [nginx-app](https://github.com/grafana/grafana/tree/master/examples/nginx-app) + - [example-app](https://github.com/grafana/example-app) -## Publish your plugin -We are currently working on this. diff --git a/docs/sources/plugins/installation.md b/docs/sources/plugins/installation.md index d3a8013ce2b..7a4ed1889f9 100644 --- a/docs/sources/plugins/installation.md +++ b/docs/sources/plugins/installation.md @@ -17,30 +17,30 @@ On Linux systems the grafana-cli will assume that the grafana plugin directory i List available plugins ``` -grafana-cli list-remote +grafana-cli plugins list-remote ``` Install a plugin type ``` -grafana-cli install +grafana-cli plugins install ``` List installed plugins ``` -grafana-cli ls +grafana-cli plugins ls ``` Upgrade all installed plugins ``` -grafana-cli upgrade-all +grafana-cli plugins upgrade-all ``` Upgrade one plugin ``` -grafana-cli upgrade +grafana-cli plugins upgrade ``` Remove one plugin ``` -grafana-cli remove +grafana-cli plugins remove ``` diff --git a/docs/sources/reference/admin.md b/docs/sources/reference/admin.md index c5d5073020d..266fc998092 100644 --- a/docs/sources/reference/admin.md +++ b/docs/sources/reference/admin.md @@ -30,7 +30,8 @@ From the Grafana Server Admin page, you can access the System Info page which su ## Why would I have multiple Organizations? -In many cases, a Grafana installation will only have one Organization. There's no need to create multiple Organizations -if you want all your users to have access to the same set of dashboards and data. In a multitenant deployment, +Organizations in Grafana are best suited for a **multi-tenant deployment**. In a multi-tenant deployment, Organizations can be used to provide a full Grafana experience to different sets of users from a single Grafana instance, -at the convenience of the Grafana Administrator. +at the convenience of the Grafana Administrator. + +In most cases, a Grafana installation will only have **one** Organization. Since dashboards, data sources and other configuration items are not shared between organizations, there's no need to create multiple Organizations if you want all your users to have access to the same set of dashboards and data. diff --git a/pkg/api/api.go b/pkg/api/api.go index 761f9244ea7..06aee594d42 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -80,7 +80,7 @@ func Register(r *macaron.Macaron) { r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot) r.Get("/api/snapshot/shared-options/", GetSharingOptions) r.Get("/api/snapshots/:key", GetDashboardSnapshot) - r.Get("/api/snapshots-delete/:key", DeleteDashboardSnapshot) + r.Get("/api/snapshots-delete/:key", reqEditorRole, DeleteDashboardSnapshot) // api renew session based on remember cookie r.Get("/api/login/ping", quota("session"), LoginApiPing) diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index 88683155006..53c911c10fa 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -3,18 +3,18 @@ package dtos import "github.com/grafana/grafana/pkg/plugins" type PluginSetting struct { - Name string `json:"name"` - Type string `json:"type"` - Id string `json:"id"` - Enabled bool `json:"enabled"` - Pinned bool `json:"pinned"` - Module string `json:"module"` - BaseUrl string `json:"baseUrl"` - Info *plugins.PluginInfo `json:"info"` - Pages []*plugins.AppPluginPage `json:"pages"` - Includes []*plugins.PluginInclude `json:"includes"` - Dependencies *plugins.PluginDependencies `json:"dependencies"` - JsonData map[string]interface{} `json:"jsonData"` + Name string `json:"name"` + Type string `json:"type"` + Id string `json:"id"` + Enabled bool `json:"enabled"` + Pinned bool `json:"pinned"` + Module string `json:"module"` + BaseUrl string `json:"baseUrl"` + Info *plugins.PluginInfo `json:"info"` + Includes []*plugins.PluginInclude `json:"includes"` + Dependencies *plugins.PluginDependencies `json:"dependencies"` + JsonData map[string]interface{} `json:"jsonData"` + DefaultNavUrl string `json:"defaultNavUrl"` } type PluginListItem struct { diff --git a/pkg/api/index.go b/pkg/api/index.go index 691c50f04f4..a376f9504a4 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -56,8 +56,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR { dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Divider: true}) - dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Url: setting.AppSubUrl + "/dashboard/new"}) - dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Url: setting.AppSubUrl + "/import/dashboard"}) + dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Icon: "fa fa-plus", Url: setting.AppSubUrl + "/dashboard/new"}) + dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Icon: "fa fa-download", Url: setting.AppSubUrl + "/import/dashboard"}) } data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ @@ -88,22 +88,35 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { for _, plugin := range enabledPlugins.Apps { if plugin.Pinned { - pageLink := &dtos.NavLink{ + appLink := &dtos.NavLink{ Text: plugin.Name, - Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit", + Url: plugin.DefaultNavUrl, Img: plugin.Info.Logos.Small, } - for _, page := range plugin.Pages { - if !page.SuppressNav { - pageLink.Children = append(pageLink.Children, &dtos.NavLink{ - Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/page/" + page.Slug, - Text: page.Name, - }) + for _, include := range plugin.Includes { + if include.Type == "page" && include.AddToNav { + link := &dtos.NavLink{ + Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/page/" + include.Slug, + Text: include.Name, + } + appLink.Children = append(appLink.Children, link) + } + if include.Type == "dashboard" && include.AddToNav { + link := &dtos.NavLink{ + Url: setting.AppSubUrl + "/dashboard/db/" + include.Slug, + Text: include.Name, + } + appLink.Children = append(appLink.Children, link) } } - data.MainNavLinks = append(data.MainNavLinks, pageLink) + if c.OrgRole == m.ROLE_ADMIN { + appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true}) + appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"}) + } + + data.MainNavLinks = append(data.MainNavLinks, appLink) } } @@ -113,10 +126,10 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin", Children: []*dtos.NavLink{ - {Text: "Global Users", Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/users"}, - {Text: "Global Orgs", Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/orgs"}, - {Text: "Server Settings", Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/settings"}, - {Text: "Server Stats", Icon: "fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/stats"}, + {Text: "Global Users", Url: setting.AppSubUrl + "/admin/users"}, + {Text: "Global Orgs", Url: setting.AppSubUrl + "/admin/orgs"}, + {Text: "Server Settings", Url: setting.AppSubUrl + "/admin/settings"}, + {Text: "Server Stats", Url: setting.AppSubUrl + "/admin/stats"}, }, }) } diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 56d0d99296c..0411c0746ef 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -72,18 +72,15 @@ func GetPluginSettingById(c *middleware.Context) Response { } else { dto := &dtos.PluginSetting{ - Type: def.Type, - Id: def.Id, - Name: def.Name, - Info: &def.Info, - Dependencies: &def.Dependencies, - Includes: def.Includes, - BaseUrl: def.BaseUrl, - Module: def.Module, - } - - if app, exists := plugins.Apps[pluginId]; exists { - dto.Pages = app.Pages + Type: def.Type, + Id: def.Id, + Name: def.Name, + Info: &def.Info, + Dependencies: &def.Dependencies, + Includes: def.Includes, + BaseUrl: def.BaseUrl, + Module: def.Module, + DefaultNavUrl: def.DefaultNavUrl, } query := m.GetPluginSettingByIdQuery{PluginId: pluginId, OrgId: c.OrgId} diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index f1b36c90ef2..55a0e2660ec 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -22,7 +22,7 @@ func runCommand(command func(commandLine CommandLine) error) func(context *cli.C } } -var Commands = []cli.Command{ +var pluginCommands = []cli.Command{ { Name: "install", Usage: "install ", @@ -49,3 +49,11 @@ var Commands = []cli.Command{ Action: runCommand(removeCommand), }, } + +var Commands = []cli.Command{ + { + Name: "plugins", + Usage: "Manage plugins for grafana", + Subcommands: pluginCommands, + }, +} diff --git a/pkg/cmd/grafana-cli/main.go b/pkg/cmd/grafana-cli/main.go index b277714fe9b..2b3b94f1751 100644 --- a/pkg/cmd/grafana-cli/main.go +++ b/pkg/cmd/grafana-cli/main.go @@ -12,10 +12,6 @@ import ( var version = "master" func getGrafanaPluginPath() string { - if os.Getenv("GF_PLUGIN_DIR") != "" { - return os.Getenv("GF_PLUGIN_DIR") - } - os := runtime.GOOS if os == "windows" { return "C:\\opt\\grafana\\plugins" @@ -34,14 +30,16 @@ func main() { app.Version = version app.Flags = []cli.Flag{ cli.StringFlag{ - Name: "path", - Usage: "path to the grafana installation", - Value: getGrafanaPluginPath(), + Name: "path", + Usage: "path to the grafana installation", + Value: getGrafanaPluginPath(), + EnvVar: "GF_PLUGIN_DIR", }, cli.StringFlag{ - Name: "repo", - Usage: "url to the plugin repository", - Value: "https://grafana-net.raintank.io/api/plugins", + Name: "repo", + Usage: "url to the plugin repository", + Value: "https://grafana-net.raintank.io/api/plugins", + EnvVar: "GF_PLUGIN_REPO", }, cli.BoolFlag{ Name: "debug, d", diff --git a/pkg/plugins/app_plugin.go b/pkg/plugins/app_plugin.go index 7fc170784f3..f565b0e2b22 100644 --- a/pkg/plugins/app_plugin.go +++ b/pkg/plugins/app_plugin.go @@ -6,16 +6,9 @@ import ( "github.com/gosimple/slug" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" ) -type AppPluginPage struct { - Name string `json:"name"` - Slug string `json:"slug"` - Component string `json:"component"` - Role models.RoleType `json:"role"` - SuppressNav bool `json:"suppressNav"` -} - type AppPluginCss struct { Light string `json:"light"` Dark string `json:"dark"` @@ -23,7 +16,6 @@ type AppPluginCss struct { type AppPlugin struct { FrontendPluginBase - Pages []*AppPluginPage `json:"pages"` Routes []*AppPluginRoute `json:"routes"` FoundChildPlugins []*PluginInclude `json:"-"` @@ -84,10 +76,18 @@ func (app *AppPlugin) initApp() { } } + app.DefaultNavUrl = setting.AppSubUrl + "/plugins/" + app.Id + "/edit" + // slugify pages - for _, page := range app.Pages { - if page.Slug == "" { - page.Slug = slug.Make(page.Name) + for _, include := range app.Includes { + if include.Slug == "" { + include.Slug = slug.Make(include.Name) + } + if include.Type == "page" && include.DefaultNav { + app.DefaultNavUrl = setting.AppSubUrl + "/plugins/" + app.Id + "/page/" + include.Slug + } + if include.Type == "dashboard" && include.DefaultNav { + app.DefaultNavUrl = setting.AppSubUrl + "/dashboard/db/" + include.Slug } } } diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 8443e91931d..30f794285e1 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) @@ -41,6 +42,7 @@ type PluginBase struct { IncludedInAppId string `json:"-"` PluginDir string `json:"-"` + DefaultNavUrl string `json:"-"` // cache for readme file contents Readme []byte `json:"-"` @@ -74,10 +76,16 @@ type PluginDependencies struct { } type PluginInclude struct { - Name string `json:"name"` - Path string `json:"path"` - Type string `json:"type"` - Id string `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Type string `json:"type"` + Component string `json:"component"` + Role models.RoleType `json:"role"` + AddToNav bool `json:"addToNav"` + DefaultNav bool `json:"defaultNav"` + Slug string `json:"slug"` + + Id string `json:"-"` } type PluginDependencyItem struct { diff --git a/public/app/core/components/navbar/navbar.html b/public/app/core/components/navbar/navbar.html index cbc0472c373..8c050174201 100644 --- a/public/app/core/components/navbar/navbar.html +++ b/public/app/core/components/navbar/navbar.html @@ -8,9 +8,9 @@ - - - {{::ctrl.title}} + + + {{ctrl.title}}
diff --git a/public/app/core/components/sidemenu/sidemenu.html b/public/app/core/components/sidemenu/sidemenu.html index 7fa5cc56647..4ff30c92f47 100644 --- a/public/app/core/components/sidemenu/sidemenu.html +++ b/public/app/core/components/sidemenu/sidemenu.html @@ -40,7 +40,10 @@ diff --git a/public/app/core/components/sidemenu/sidemenu.ts b/public/app/core/components/sidemenu/sidemenu.ts index 4c0e100f85c..a47704f9d7d 100644 --- a/public/app/core/components/sidemenu/sidemenu.ts +++ b/public/app/core/components/sidemenu/sidemenu.ts @@ -86,7 +86,7 @@ export class SideMenuCtrl { switchOrg(orgId) { this.backendSrv.post('/api/user/using/' + orgId).then(() => { - window.location.href = window.location.href; + window.location.href = `${config.appSubUrl}/`; }); }; } diff --git a/public/app/core/services/backend_srv.js b/public/app/core/services/backend_srv.js index 6d0d112ba26..ff3784ab45e 100644 --- a/public/app/core/services/backend_srv.js +++ b/public/app/core/services/backend_srv.js @@ -105,6 +105,13 @@ function (angular, _, coreModule, config) { }); } + //populate error obj on Internal Error + if (_.isString(err.data) && err.status === 500) { + err.data = { + error: err.statusText + }; + } + // for Prometheus if (!err.data.message && _.isString(err.data.error)) { err.data.message = err.data.error; diff --git a/public/app/features/org/partials/newOrg.html b/public/app/features/org/partials/newOrg.html index 916c73177b7..88c348db78f 100644 --- a/public/app/features/org/partials/newOrg.html +++ b/public/app/features/org/partials/newOrg.html @@ -1,21 +1,21 @@ -
-
+
+ -

Add Organization

+

Each organization contains their own dashboards, data sources and configuration, and cannot be shared between orgs. While users may belong to more than one, mutiple organization are most frequently used in multi-tenant deployments.

-
-
- Org. name - -
-
-
- -
-
+
+
+ Org. name + +
+
+
+ +
- diff --git a/public/app/features/org/partials/orgDetails.html b/public/app/features/org/partials/orgDetails.html index d0f99e23eb7..fe988570e50 100644 --- a/public/app/features/org/partials/orgDetails.html +++ b/public/app/features/org/partials/orgDetails.html @@ -3,7 +3,7 @@

General

diff --git a/public/app/features/panel/panel_menu.js b/public/app/features/panel/panel_menu.js index bdd4a9ce754..f75aacb32f1 100644 --- a/public/app/features/panel/panel_menu.js +++ b/public/app/features/panel/panel_menu.js @@ -56,6 +56,7 @@ function (angular, $, _, Tether) { template += ''; }); diff --git a/public/app/features/plugins/partials/ds_edit.html b/public/app/features/plugins/partials/ds_edit.html index 1470a312bb0..1866fad50a0 100644 --- a/public/app/features/plugins/partials/ds_edit.html +++ b/public/app/features/plugins/partials/ds_edit.html @@ -4,8 +4,8 @@