diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 0c2bd7366e7..d0ce39cf482 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -46,7 +46,7 @@ Then you can override them using: ## instance_name Set the name of the grafana-server instance. Used in logging and internal metrics and in -clustering info. Defaults to: `${HOSTNAME}, which will be replaced with +clustering info. Defaults to: `${HOSTNAME}`, which will be replaced with environment variable `HOSTNAME`, if that is empty or does not exist Grafana will try to use system calls to get the machine name. diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 92f11764aa4..360d43512f0 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -10,20 +10,13 @@ page_keywords: grafana, installation, debian, ubuntu, guide Description | Download ------------ | ------------- -Stable .deb for Debian-based Linux | [3.0.4](https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.4-1464167696_amd64.deb) -Beta .deb for Debian-based Linux | [3.1.0-beta1](https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.0-1466666977beta1_amd64.deb) +Stable .deb for Debian-based Linux | [3.1.0](https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.0-1468321182_amd64.deb) ## Install Stable - $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.4-1464167696_amd64.deb + $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.0-1468321182_amd64.deb $ sudo apt-get install -y adduser libfontconfig - $ sudo dpkg -i grafana_3.0.4-1464167696_amd64.deb - -## Install 3.1 beta - - $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.0-1466666977beta1_amd64.deb - $ sudo apt-get install -y adduser libfontconfig - $ sudo dpkg -i grafana_3.1.0-1466666977beta1_amd64.deb + $ sudo dpkg -i grafana_3.1.0-1468321182_amd64.deb ## APT Repository diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 0e423eb0af4..c08521a7538 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -10,42 +10,24 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide Description | Download ------------ | ------------- -Stable .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [3.0.4 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.4-1464167696.x86_64.rpm) -Beta .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [3.1.0-beta1 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.0-1466666977beta1.x86_64.rpm) +Stable .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [3.1.0 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.0-1468321182.x86_64.rpm) ## Install Latest Stable You can install Grafana using Yum directly. - $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.4-1464167696.x86_64.rpm + $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.0-1468321182.x86_64.rpm Or install manually using `rpm`. #### On CentOS / Fedora / Redhat: $ sudo yum install initscripts fontconfig - $ sudo rpm -Uvh grafana-3.0.4-1464167696.x86_64.rpm + $ sudo rpm -Uvh grafana-3.1.0-1468321182.x86_64.rpm #### On OpenSuse: - $ sudo rpm -i --nodeps grafana-3.0.4-1464167696.x86_64.rpm - -## Install 3.1 Beta - -You can install Grafana using Yum directly. - - $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.0-1466666977beta1.x86_64.rpm - -Or install manually using `rpm`. - -#### On CentOS / Fedora / Redhat: - - $ sudo yum install initscripts fontconfig - $ sudo rpm -Uvh https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.0-1466666977beta1.x86_64.rpm - -#### On OpenSuse: - - $ sudo rpm -i --nodeps https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.0-1466666977beta1.x86_64.rpm + $ sudo rpm -i --nodeps grafana-3.1.0-1468321182.x86_64.rpm ## Install via YUM Repository diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 858f8cfea2d..88c92d2ffed 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -10,7 +10,7 @@ page_keywords: grafana, installation, windows guide Description | Download ------------ | ------------- -Stable Zip package for Windows | [grafana.3.0.4.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-3.0.4.windows-x64.zip) +Stable Zip package for Windows | [grafana.3.1.0.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-3.1.0.windows-x64.zip) ## Configure diff --git a/package.json b/package.json index 80ab25f8eb3..5490435309f 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "phantomjs-prebuilt": "^2.1.7", "reflect-metadata": "0.1.2", "rxjs": "5.0.0-beta.4", - "sass-lint": "^1.6.0", + "sass-lint": "^1.7.0", "systemjs": "0.19.24" }, "engines": { @@ -69,7 +69,7 @@ "dependencies": { "eventemitter3": "^1.2.0", "grunt-jscs": "~1.5.x", - "grunt-sass-lint": "^0.1.0", + "grunt-sass-lint": "^0.2.0", "grunt-sync": "^0.4.1", "karma-sinon": "^1.0.3", "lodash": "^2.4.1", diff --git a/pkg/api/api.go b/pkg/api/api.go index 4cebd56d5b1..8c45c8e6d8b 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -211,7 +211,7 @@ func Register(r *macaron.Macaron) { // Dashboard r.Group("/dashboards", func() { r.Combo("/db/:slug").Get(GetDashboard).Delete(DeleteDashboard) - r.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), PostDashboard) + r.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), wrap(PostDashboard)) r.Get("/file/:file", GetDashboardFromJsonFile) r.Get("/home", wrap(GetHomeDashboard)) r.Get("/tags", GetDashboardTags) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index cbad74444bf..041f2a7f8cf 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -109,7 +110,7 @@ func DeleteDashboard(c *middleware.Context) { c.JSON(200, resp) } -func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) { +func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { cmd.OrgId = c.OrgId if !c.IsSignedIn { @@ -122,35 +123,37 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) { if dash.Id == 0 { limitReached, err := middleware.QuotaReached(c, "dashboard") if err != nil { - c.JsonApiErr(500, "failed to get quota", err) - return + return ApiError(500, "failed to get quota", err) } if limitReached { - c.JsonApiErr(403, "Quota reached", nil) - return + return ApiError(403, "Quota reached", nil) } } err := bus.Dispatch(&cmd) if err != nil { if err == m.ErrDashboardWithSameNameExists { - c.JSON(412, util.DynMap{"status": "name-exists", "message": err.Error()}) - return + return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()}) } if err == m.ErrDashboardVersionMismatch { - c.JSON(412, util.DynMap{"status": "version-mismatch", "message": err.Error()}) - return + return Json(412, util.DynMap{"status": "version-mismatch", "message": err.Error()}) + } + if pluginErr, ok := err.(m.UpdatePluginDashboardError); ok { + message := "The dashboard belongs to plugin " + pluginErr.PluginId + "." + // look up plugin name + if pluginDef, exist := plugins.Plugins[pluginErr.PluginId]; exist { + message = "The dashboard belongs to plugin " + pluginDef.Name + "." + } + return Json(412, util.DynMap{"status": "plugin-dashboard", "message": message}) } if err == m.ErrDashboardNotFound { - c.JSON(404, util.DynMap{"status": "not-found", "message": err.Error()}) - return + return Json(404, util.DynMap{"status": "not-found", "message": err.Error()}) } - c.JsonApiErr(500, "Failed to save dashboard", err) - return + return ApiError(500, "Failed to save dashboard", err) } c.TimeRequest(metrics.M_Api_Dashboard_Save) - c.JSON(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version}) + return Json(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version}) } func canEditDashboard(role m.RoleType) bool { diff --git a/pkg/api/playlist_play.go b/pkg/api/playlist_play.go index f3bae6cbcd3..e4feb3442fb 100644 --- a/pkg/api/playlist_play.go +++ b/pkg/api/playlist_play.go @@ -18,7 +18,7 @@ func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, e return result, err } - for _, item := range *dashboardQuery.Result { + for _, item := range dashboardQuery.Result { result = append(result, m.PlaylistDashboardDto{ Id: item.Id, Slug: item.Slug, diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 610f29e70aa..53ba11c0237 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -17,6 +17,14 @@ var ( ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else") ) +type UpdatePluginDashboardError struct { + PluginId string +} + +func (d UpdatePluginDashboardError) Error() string { + return "Dashboard belong to plugin" +} + var ( DashTypeJson = "file" DashTypeDB = "db" @@ -26,11 +34,12 @@ var ( // Dashboard model type Dashboard struct { - Id int64 - Slug string - OrgId int64 - GnetId int64 - Version int + Id int64 + Slug string + OrgId int64 + GnetId int64 + Version int + PluginId string Created time.Time Updated time.Time @@ -95,6 +104,7 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { dash.UpdatedBy = cmd.UserId dash.OrgId = cmd.OrgId + dash.PluginId = cmd.PluginId dash.UpdateSlug() return dash } @@ -119,6 +129,7 @@ type SaveDashboardCommand struct { UserId int64 `json:"userId"` OrgId int64 `json:"-"` Overwrite bool `json:"overwrite"` + PluginId string `json:"-"` Result *Dashboard } @@ -151,7 +162,13 @@ type GetDashboardTagsQuery struct { type GetDashboardsQuery struct { DashboardIds []int64 - Result *[]Dashboard + Result []*Dashboard +} + +type GetDashboardsByPluginIdQuery struct { + OrgId int64 + PluginId string + Result []*Dashboard } type GetDashboardSlugByIdQuery struct { diff --git a/pkg/models/plugin_settings.go b/pkg/models/plugin_settings.go index d030c125ba9..21c48411797 100644 --- a/pkg/models/plugin_settings.go +++ b/pkg/models/plugin_settings.go @@ -20,6 +20,7 @@ type PluginSetting struct { Pinned bool JsonData map[string]interface{} SecureJsonData SecureJsonData + PluginVersion string Created time.Time Updated time.Time @@ -44,11 +45,19 @@ type UpdatePluginSettingCmd struct { Pinned bool `json:"pinned"` JsonData map[string]interface{} `json:"jsonData"` SecureJsonData map[string]string `json:"secureJsonData"` + PluginVersion string `json:"version"` PluginId string `json:"-"` OrgId int64 `json:"-"` } +// specific command, will only update version +type UpdatePluginSettingVersionCmd struct { + PluginVersion string + PluginId string `json:"-"` + OrgId int64 `json:"-"` +} + func (cmd *UpdatePluginSettingCmd) GetEncryptedJsonData() SecureJsonData { encrypted := make(SecureJsonData) for key, data := range cmd.SecureJsonData { @@ -65,10 +74,11 @@ type GetPluginSettingsQuery struct { } type PluginSettingInfoDTO struct { - OrgId int64 - PluginId string - Enabled bool - Pinned bool + OrgId int64 + PluginId string + Enabled bool + Pinned bool + PluginVersion string } type GetPluginSettingByIdQuery struct { @@ -76,3 +86,9 @@ type GetPluginSettingByIdQuery struct { OrgId int64 Result *PluginSetting } + +type PluginStateChangedEvent struct { + PluginId string + OrgId int64 + Enabled bool +} diff --git a/pkg/plugins/dashboard_importer.go b/pkg/plugins/dashboard_importer.go index 8f6998d344d..1b3e4bac182 100644 --- a/pkg/plugins/dashboard_importer.go +++ b/pkg/plugins/dashboard_importer.go @@ -68,6 +68,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error { OrgId: cmd.OrgId, UserId: cmd.UserId, Overwrite: cmd.Overwrite, + PluginId: cmd.PluginId, } if err := bus.Dispatch(&saveCmd); err != nil { diff --git a/pkg/plugins/dashboards.go b/pkg/plugins/dashboards.go index 1a160fe6632..5d26766b8e5 100644 --- a/pkg/plugins/dashboards.go +++ b/pkg/plugins/dashboards.go @@ -14,10 +14,12 @@ type PluginDashboardInfoDTO struct { Title string `json:"title"` Imported bool `json:"imported"` ImportedUri string `json:"importedUri"` + Slug string `json:"slug"` ImportedRevision int64 `json:"importedRevision"` Revision int64 `json:"revision"` Description string `json:"description"` Path string `json:"path"` + Removed bool `json:"removed"` } func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDTO, error) { @@ -29,14 +31,53 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT result := make([]*PluginDashboardInfoDTO, 0) + // load current dashboards + query := m.GetDashboardsByPluginIdQuery{OrgId: orgId, PluginId: pluginId} + if err := bus.Dispatch(&query); err != nil { + return nil, err + } + + existingMatches := make(map[int64]bool) + for _, include := range plugin.Includes { - if include.Type == PluginTypeDashboard { - if dashInfo, err := getDashboardImportStatus(orgId, plugin, include.Path); err != nil { - return nil, err - } else { - result = append(result, dashInfo) + if include.Type != PluginTypeDashboard { + continue + } + + res := &PluginDashboardInfoDTO{} + var dashboard *m.Dashboard + var err error + + if dashboard, err = loadPluginDashboard(plugin.Id, include.Path); err != nil { + return nil, err + } + + res.Path = include.Path + res.PluginId = plugin.Id + res.Title = dashboard.Title + res.Revision = dashboard.Data.Get("revision").MustInt64(1) + + // find existing dashboard + for _, existingDash := range query.Result { + if existingDash.Slug == dashboard.Slug { + res.Imported = true + res.ImportedUri = "db/" + existingDash.Slug + res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1) + existingMatches[existingDash.Id] = true } } + + result = append(result, res) + } + + // find deleted dashboards + for _, dash := range query.Result { + if _, exists := existingMatches[dash.Id]; !exists { + result = append(result, &PluginDashboardInfoDTO{ + Slug: dash.Slug, + Removed: true, + }) + } } return result, nil @@ -64,33 +105,3 @@ func loadPluginDashboard(pluginId, path string) (*m.Dashboard, error) { return m.NewDashboardFromJson(data), nil } - -func getDashboardImportStatus(orgId int64, plugin *PluginBase, path string) (*PluginDashboardInfoDTO, error) { - res := &PluginDashboardInfoDTO{} - - var dashboard *m.Dashboard - var err error - - if dashboard, err = loadPluginDashboard(plugin.Id, path); err != nil { - return nil, err - } - - res.Path = path - res.PluginId = plugin.Id - res.Title = dashboard.Title - res.Revision = dashboard.Data.Get("revision").MustInt64(1) - - query := m.GetDashboardQuery{OrgId: orgId, Slug: dashboard.Slug} - - if err := bus.Dispatch(&query); err != nil { - if err != m.ErrDashboardNotFound { - return nil, err - } - } else { - res.Imported = true - res.ImportedUri = "db/" + query.Result.Slug - res.ImportedRevision = query.Result.Data.Get("revision").MustInt64(1) - } - - return res, nil -} diff --git a/pkg/plugins/dashboards_test.go b/pkg/plugins/dashboards_test.go index 98693349b4c..980d7bb91bd 100644 --- a/pkg/plugins/dashboards_test.go +++ b/pkg/plugins/dashboards_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" @@ -31,6 +32,17 @@ func TestPluginDashboards(t *testing.T) { return m.ErrDashboardNotFound }) + bus.AddHandler("test", func(query *m.GetDashboardsByPluginIdQuery) error { + var data = simplejson.New() + data.Set("title", "Nginx Connections") + data.Set("revision", 22) + + query.Result = []*m.Dashboard{ + {Slug: "nginx-connections", Data: data}, + } + return nil + }) + dashboards, err := GetPluginDashboards(1, "test-app") So(err, ShouldBeNil) @@ -41,12 +53,12 @@ func TestPluginDashboards(t *testing.T) { Convey("should include installed version info", func() { So(dashboards[0].Title, ShouldEqual, "Nginx Connections") - //So(dashboards[0].Revision, ShouldEqual, "1.5") - //So(dashboards[0].InstalledRevision, ShouldEqual, "1.1") - //So(dashboards[0].InstalledUri, ShouldEqual, "db/nginx-connections") + So(dashboards[0].Revision, ShouldEqual, 25) + So(dashboards[0].ImportedRevision, ShouldEqual, 22) + So(dashboards[0].ImportedUri, ShouldEqual, "db/nginx-connections") - //So(dashboards[1].Revision, ShouldEqual, "2.0") - //So(dashboards[1].InstalledRevision, ShouldEqual, "") + So(dashboards[1].Revision, ShouldEqual, 2) + So(dashboards[1].ImportedRevision, ShouldEqual, 0) }) }) diff --git a/pkg/plugins/dashboards_updater.go b/pkg/plugins/dashboards_updater.go new file mode 100644 index 00000000000..52a623e73dd --- /dev/null +++ b/pkg/plugins/dashboards_updater.go @@ -0,0 +1,139 @@ +package plugins + +import ( + "time" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +func init() { + bus.AddEventListener(handlePluginStateChanged) +} + +func updateAppDashboards() { + time.Sleep(time.Second * 5) + + plog.Debug("Looking for App Dashboard Updates") + + query := m.GetPluginSettingsQuery{OrgId: 0} + + if err := bus.Dispatch(&query); err != nil { + plog.Error("Failed to get all plugin settings", "error", err) + return + } + + for _, pluginSetting := range query.Result { + // ignore disabled plugins + if !pluginSetting.Enabled { + continue + } + + if pluginDef, exist := Plugins[pluginSetting.PluginId]; exist { + if pluginDef.Info.Version != pluginSetting.PluginVersion { + syncPluginDashboards(pluginDef, pluginSetting.OrgId) + } + } + } +} + +func autoUpdateAppDashboard(pluginDashInfo *PluginDashboardInfoDTO, orgId int64) error { + if dash, err := loadPluginDashboard(pluginDashInfo.PluginId, pluginDashInfo.Path); err != nil { + return err + } else { + plog.Info("Auto updating App dashboard", "dashboard", dash.Title, "newRev", pluginDashInfo.Revision, "oldRev", pluginDashInfo.ImportedRevision) + updateCmd := ImportDashboardCommand{ + OrgId: orgId, + PluginId: pluginDashInfo.PluginId, + Overwrite: true, + Dashboard: dash.Data, + UserId: 0, + Path: pluginDashInfo.Path, + } + + if err := bus.Dispatch(&updateCmd); err != nil { + return err + } + } + return nil +} + +func syncPluginDashboards(pluginDef *PluginBase, orgId int64) { + plog.Info("Syncing plugin dashboards to DB", "pluginId", pluginDef.Id) + + // Get plugin dashboards + dashboards, err := GetPluginDashboards(orgId, pluginDef.Id) + + if err != nil { + plog.Error("Failed to load app dashboards", "error", err) + return + } + + // Update dashboards with updated revisions + for _, dash := range dashboards { + // remove removed ones + if dash.Removed { + plog.Info("Deleting plugin dashboard", "pluginId", pluginDef.Id, "dashboard", dash.Slug) + + deleteCmd := m.DeleteDashboardCommand{OrgId: orgId, Slug: dash.Slug} + if err := bus.Dispatch(&deleteCmd); err != nil { + plog.Error("Failed to auto update app dashboard", "pluginId", pluginDef.Id, "error", err) + return + } + + continue + } + + // update updated ones + if dash.ImportedRevision != dash.Revision { + if err := autoUpdateAppDashboard(dash, orgId); err != nil { + plog.Error("Failed to auto update app dashboard", "pluginId", pluginDef.Id, "error", err) + return + } + } + } + + // update version in plugin_setting table to mark that we have processed the update + query := m.GetPluginSettingByIdQuery{PluginId: pluginDef.Id, OrgId: orgId} + if err := bus.Dispatch(&query); err != nil { + plog.Error("Failed to read plugin setting by id", "error", err) + return + } + + appSetting := query.Result + cmd := m.UpdatePluginSettingVersionCmd{ + OrgId: appSetting.OrgId, + PluginId: appSetting.PluginId, + PluginVersion: pluginDef.Info.Version, + } + + if err := bus.Dispatch(&cmd); err != nil { + plog.Error("Failed to update plugin setting version", "error", err) + } +} + +func handlePluginStateChanged(event *m.PluginStateChangedEvent) error { + plog.Info("Plugin state changed", "pluginId", event.PluginId, "enabled", event.Enabled) + + if event.Enabled { + syncPluginDashboards(Plugins[event.PluginId], event.OrgId) + } else { + query := m.GetDashboardsByPluginIdQuery{PluginId: event.PluginId, OrgId: event.OrgId} + + if err := bus.Dispatch(&query); err != nil { + return err + } else { + for _, dash := range query.Result { + deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Slug: dash.Slug} + + plog.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug) + + if err := bus.Dispatch(&deleteCmd); err != nil { + return err + } + } + } + } + + return nil +} diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index cf931066cbb..b6c3639cbbf 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -77,6 +77,8 @@ func Init() error { } go StartPluginUpdateChecker() + go updateAppDashboards() + return nil } diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index ef36fd75ab6..7a0971fcdfd 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -19,6 +19,7 @@ func init() { bus.AddHandler("sql", SearchDashboards) bus.AddHandler("sql", GetDashboardTags) bus.AddHandler("sql", GetDashboardSlugById) + bus.AddHandler("sql", GetDashboardsByPluginId) } func SaveDashboard(cmd *m.SaveDashboardCommand) error { @@ -45,6 +46,11 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { return m.ErrDashboardVersionMismatch } } + + // do not allow plugin dashboard updates without overwrite flag + if existing.PluginId != "" && cmd.Overwrite == false { + return m.UpdatePluginDashboardError{PluginId: existing.PluginId} + } } sameTitleExists, err := sess.Where("org_id=? AND slug=?", dash.OrgId, dash.Slug).Get(&sameTitle) @@ -245,10 +251,23 @@ func GetDashboards(query *m.GetDashboardsQuery) error { return m.ErrCommandValidationFailed } - var dashboards = make([]m.Dashboard, 0) + var dashboards = make([]*m.Dashboard, 0) err := x.In("id", query.DashboardIds).Find(&dashboards) - query.Result = &dashboards + query.Result = dashboards + + if err != nil { + return err + } + + return nil +} + +func GetDashboardsByPluginId(query *m.GetDashboardsByPluginIdQuery) error { + var dashboards = make([]*m.Dashboard, 0) + + err := x.Where("org_id=? AND plugin_id=?", query.OrgId, query.PluginId).Find(&dashboards) + query.Result = dashboards if err != nil { return err diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index 38c25a315f2..4f286dce68a 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -111,4 +111,13 @@ func addDashboardMigration(mg *Migrator) { mg.AddMigration("Add index for gnetId in dashboard", NewAddIndexMigration(dashboardV2, &Index{ Cols: []string{"gnet_id"}, Type: IndexType, })) + + // add column to store plugin_id + mg.AddMigration("Add column plugin_id in dashboard", NewAddColumnMigration(dashboardV2, &Column{ + Name: "plugin_id", Type: DB_NVarchar, Nullable: true, Length: 255, + })) + + mg.AddMigration("Add index for plugin_id in dashboard", NewAddIndexMigration(dashboardV2, &Index{ + Cols: []string{"org_id", "plugin_id"}, Type: IndexType, + })) } diff --git a/pkg/services/sqlstore/migrations/plugin_setting.go b/pkg/services/sqlstore/migrations/plugin_setting.go index 4a8729691d3..0700ab67d2f 100644 --- a/pkg/services/sqlstore/migrations/plugin_setting.go +++ b/pkg/services/sqlstore/migrations/plugin_setting.go @@ -26,4 +26,10 @@ func addAppSettingsMigration(mg *Migrator) { //------- indexes ------------------ addTableIndicesMigrations(mg, "v1", pluginSettingTable) + + // add column to store installed version + mg.AddMigration("Add column plugin_version to plugin_settings", NewAddColumnMigration(pluginSettingTable, &Column{ + Name: "plugin_version", Type: DB_NVarchar, Nullable: true, Length: 50, + })) + } diff --git a/pkg/services/sqlstore/plugin_setting.go b/pkg/services/sqlstore/plugin_setting.go index ec0b9b2e2d7..8121b8c7b4b 100644 --- a/pkg/services/sqlstore/plugin_setting.go +++ b/pkg/services/sqlstore/plugin_setting.go @@ -13,14 +13,20 @@ func init() { bus.AddHandler("sql", GetPluginSettings) bus.AddHandler("sql", GetPluginSettingById) bus.AddHandler("sql", UpdatePluginSetting) + bus.AddHandler("sql", UpdatePluginSettingVersion) } func GetPluginSettings(query *m.GetPluginSettingsQuery) error { - sql := `SELECT org_id, plugin_id, enabled, pinned - FROM plugin_setting - WHERE org_id=?` + sql := `SELECT org_id, plugin_id, enabled, pinned, plugin_version + FROM plugin_setting ` + params := make([]interface{}, 0) - sess := x.Sql(sql, query.OrgId) + if query.OrgId != 0 { + sql += "WHERE org_id=?" + params = append(params, query.OrgId) + } + + sess := x.Sql(sql, params...) query.Result = make([]*m.PluginSettingInfoDTO, 0) return sess.Find(&query.Result) } @@ -51,22 +57,52 @@ func UpdatePluginSetting(cmd *m.UpdatePluginSettingCmd) error { Enabled: cmd.Enabled, Pinned: cmd.Pinned, JsonData: cmd.JsonData, + PluginVersion: cmd.PluginVersion, SecureJsonData: cmd.GetEncryptedJsonData(), Created: time.Now(), Updated: time.Now(), } + + // add state change event on commit success + sess.events = append(sess.events, &m.PluginStateChangedEvent{ + PluginId: cmd.PluginId, + OrgId: cmd.OrgId, + Enabled: cmd.Enabled, + }) + _, err = sess.Insert(&pluginSetting) return err } else { for key, data := range cmd.SecureJsonData { pluginSetting.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey) } + + // add state change event on commit success + if pluginSetting.Enabled != cmd.Enabled { + sess.events = append(sess.events, &m.PluginStateChangedEvent{ + PluginId: cmd.PluginId, + OrgId: cmd.OrgId, + Enabled: cmd.Enabled, + }) + } + pluginSetting.Updated = time.Now() pluginSetting.Enabled = cmd.Enabled pluginSetting.JsonData = cmd.JsonData pluginSetting.Pinned = cmd.Pinned + pluginSetting.PluginVersion = cmd.PluginVersion + _, err = sess.Id(pluginSetting.Id).Update(&pluginSetting) return err } }) } + +func UpdatePluginSettingVersion(cmd *m.UpdatePluginSettingVersionCmd) error { + return inTransaction2(func(sess *session) error { + + _, err := sess.Exec("UPDATE plugin_setting SET plugin_version=? WHERE org_id=? AND plugin_id=?", cmd.PluginVersion, cmd.OrgId, cmd.PluginId) + return err + + }) +} diff --git a/public/app/core/components/search/search.html b/public/app/core/components/search/search.html index 6344a26c886..e8e2179ce5c 100644 --- a/public/app/core/components/search/search.html +++ b/public/app/core/components/search/search.html @@ -72,6 +72,10 @@ Import + + Find dashboards on + +
diff --git a/public/app/core/services/alert_srv.ts b/public/app/core/services/alert_srv.ts index 3003e59c74b..edfff2e8d00 100644 --- a/public/app/core/services/alert_srv.ts +++ b/public/app/core/services/alert_srv.ts @@ -73,6 +73,8 @@ export class AlertSrv { scope.text = payload.text; scope.text2 = payload.text2; scope.onConfirm = payload.onConfirm; + scope.onAltAction = payload.onAltAction; + scope.altActionText = payload.altActionText; scope.icon = payload.icon || "fa-check"; scope.yesText = payload.yesText || "Yes"; scope.noText = payload.noText || "Cancel"; diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index 0a3c1b95f95..937d5266c56 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -22,6 +22,7 @@ function (angular, $, _, moment) { this.id = data.id || null; this.title = data.title || 'No Title'; + this.autoUpdate = data.autoUpdate; this.description = data.description; this.tags = data.tags || []; this.style = data.style || "dark"; diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index 62f598e1cbd..7d25e352e9e 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -134,6 +134,25 @@ export class DashNavCtrl { } }); } + + if (err.data && err.data.status === "plugin-dashboard") { + err.isHandled = true; + + $scope.appEvent('confirm-modal', { + title: 'Plugin Dashboard', + text: err.data.message, + text2: 'Your changes will be lost when you update the plugin. Use Save As to create custom version.', + yesText: "Overwrite", + icon: "fa-warning", + altActionText: "Save As", + onAltAction: function() { + $scope.saveDashboardAs(); + }, + onConfirm: function() { + $scope.saveDashboard({overwrite: true}); + } + }); + } }; $scope.deleteDashboard = function() { diff --git a/public/app/features/dashboard/saveDashboardAsCtrl.js b/public/app/features/dashboard/saveDashboardAsCtrl.js index 92c93b7e485..211863b66e6 100644 --- a/public/app/features/dashboard/saveDashboardAsCtrl.js +++ b/public/app/features/dashboard/saveDashboardAsCtrl.js @@ -12,6 +12,8 @@ function (angular) { $scope.clone.id = null; $scope.clone.editable = true; $scope.clone.title = $scope.clone.title + " Copy"; + // remove auto update + delete $scope.clone.autoUpdate; }; function saveDashboard(options) { @@ -37,8 +39,9 @@ function (angular) { err.isHandled = true; $scope.appEvent('confirm-modal', { - title: 'Another dashboard with the same name exists', - text: "Would you still like to save this dashboard?", + title: 'Conflict', + text: 'Dashboard with the same name exists.', + text2: 'Would you still like to save this dashboard?', yesText: "Save & Overwrite", icon: "fa-warning", onConfirm: function() { diff --git a/public/app/features/plugins/import_list/import_list.html b/public/app/features/plugins/import_list/import_list.html index 746109970e0..715f5b3be74 100644 --- a/public/app/features/plugins/import_list/import_list.html +++ b/public/app/features/plugins/import_list/import_list.html @@ -14,20 +14,19 @@