diff --git a/CHANGELOG.md b/CHANGELOG.md index 368a8da5b34..1a66686fea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,12 @@ * **Localization**: Week start day now dependant on browser locale setting, closes [#3003](https://github.com/grafana/grafana/issues/3003) * **Templating**: Update panel repeats for variables that change on time refresh, closes [#5021](https://github.com/grafana/grafana/issues/5021) -# 3.1.0 stable (unreleased) +# 3.1.0 stable (2016-07-12) -### Bugfixes +### Bugfixes & Enhancements, * **User Alert Notices**: Backend error alert popups did not show properly, fixes [#5435](https://github.com/grafana/grafana/issues/5435) +* **Table**: Added sanitize HTML option to allow links in table cells, fixes [#4596](https://github.com/grafana/grafana/issues/4596) +* **Apps**: App dashboards are automatically synced to DB at startup after plugin update, fixes [#5529](https://github.com/grafana/grafana/issues/5529) # 3.1.0-beta1 (2016-06-23) 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/latest.json b/latest.json index e18c220a7b0..f27001ccc41 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "3.0.4", - "testing": "3.1.0-beta1" + "stable": "3.1.0", + "testing": "3.1.0" } 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/packaging/rpm/init.d/grafana-server b/packaging/rpm/init.d/grafana-server index c60f4fb6080..cb9bb73de7d 100755 --- a/packaging/rpm/init.d/grafana-server +++ b/packaging/rpm/init.d/grafana-server @@ -72,8 +72,6 @@ function isRunning() { case "$1" in start) - echo -n $"Starting $DESC: .... " - isRunning if [ $? -eq 0 ]; then echo "Already running." @@ -90,7 +88,7 @@ case "$1" in # Start Daemon cd $GRAFANA_HOME - su -s /bin/sh -c "nohup ${DAEMON} ${DAEMON_OPTS} >> /dev/null 3>&1 &" $GRAFANA_USER 2> /dev/null + action $"Starting $DESC: ..." su -s /bin/sh -c "nohup ${DAEMON} ${DAEMON_OPTS} >> /dev/null 3>&1 &" $GRAFANA_USER 2> /dev/null return=$? if [ $return -eq 0 ] then @@ -114,26 +112,25 @@ case "$1" in done fi - echo "OK" exit $return ;; stop) - echo -n "Stopping $DESC ..." + echo -n "Stopping $DESC: ..." if [ -f "$PID_FILE" ]; then killproc -p $PID_FILE -d 20 $NAME if [ $? -eq 1 ]; then - echo -n "$DESC is not running but pid file exists, cleaning up" + echo "$DESC is not running but pid file exists, cleaning up" elif [ $? -eq 3 ]; then PID="`cat $PID_FILE`" - echo -n "Failed to stop $DESC (pid $PID)" + echo "Failed to stop $DESC (pid $PID)" exit 1 fi rm -f "$PID_FILE" - echo "OK" + echo "" exit 0 else - echo -n "(not running)" + echo "(not running)" fi exit 0 ;; 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/components/sidemenu/sidemenu.html b/public/app/core/components/sidemenu/sidemenu.html index 1ee2776bfe9..975e198b009 100644 --- a/public/app/core/components/sidemenu/sidemenu.html +++ b/public/app/core/components/sidemenu/sidemenu.html @@ -45,7 +45,7 @@
  • - + Sign in diff --git a/public/app/core/components/sidemenu/sidemenu.ts b/public/app/core/components/sidemenu/sidemenu.ts index a4b1f5d454a..84154621a0a 100644 --- a/public/app/core/components/sidemenu/sidemenu.ts +++ b/public/app/core/components/sidemenu/sidemenu.ts @@ -23,13 +23,13 @@ export class SideMenuCtrl { this.mainLinks = config.bootData.mainNavLinks; this.openUserDropdown(); - this.loginUrl = '/login?redirect=' + encodeURIComponent(this.$location.path()); + this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path()); this.$scope.$on('$routeChangeSuccess', () => { if (!this.contextSrv.pinned) { this.contextSrv.sidemenu = false; } - this.loginUrl = '/login?redirect=' + encodeURIComponent(this.$location.path()); + this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path()); }); } 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/core/utils/kbn.js b/public/app/core/utils/kbn.js index fbbab141614..29cd7c49890 100644 --- a/public/app/core/utils/kbn.js +++ b/public/app/core/utils/kbn.js @@ -368,6 +368,23 @@ function($, _, moment) { return kbn.toFixed(100*size, decimals) + '%'; }; + /* Formats the value to hex. Uses float if specified decimals are not 0. + * There are two options, one with 0x, and one without */ + + kbn.valueFormats.hex = function(value, decimals) { + if (value == null) { return ""; } + return parseFloat(kbn.toFixed(value, decimals)).toString(16).toUpperCase(); + }; + + kbn.valueFormats.hex0x = function(value, decimals) { + if (value == null) { return ""; } + var hexString = kbn.valueFormats.hex(value, decimals); + if (hexString.substring(0,1) === "-") { + return "-0x" + hexString.substring(1); + } + return "0x" + hexString; + }; + // Currencies kbn.valueFormats.currencyUSD = kbn.formatBuilders.currency('$'); kbn.valueFormats.currencyGBP = kbn.formatBuilders.currency('£'); @@ -617,6 +634,8 @@ function($, _, moment) { {text: 'Humidity (%H)', value: 'humidity' }, {text: 'ppm', value: 'ppm' }, {text: 'decibel', value: 'dB' }, + {text: 'hexadecimal (0x)', value: 'hex0x' }, + {text: 'hexadecimal', value: 'hex' }, ] }, { 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 @@ - v{{dash.revision}} - -  (Imported v{{dash.importedRevision}}) - + + Revision: {{dash.revision}} + - - - diff --git a/public/app/features/plugins/partials/plugin_list.html b/public/app/features/plugins/partials/plugin_list.html index 64e511805dc..c276faae93a 100644 --- a/public/app/features/plugins/partials/plugin_list.html +++ b/public/app/features/plugins/partials/plugin_list.html @@ -5,6 +5,10 @@ -
    + + Find plugins on + + + + +
    -
      -
    1. - -
      -
      - - {{plugin.type}} -
      -
      - Update available! -
      -
      -
      -
      - -
      -
      -
      {{plugin.name}}
      -
      By {{plugin.info.author.name}}
      -
      -
      -
      -
    2. -
    -
    +
      +
    1. + +
      +
      + + {{plugin.type}} +
      +
      + Update available! +
      +
      +
      +
      + +
      +
      +
      {{plugin.name}}
      +
      By {{plugin.info.author.name}}
      +
      +
      +
      +
    2. +
    +
    diff --git a/public/app/features/plugins/plugin_edit_ctrl.ts b/public/app/features/plugins/plugin_edit_ctrl.ts index a10b5eb6e7a..c4f4953dd5a 100644 --- a/public/app/features/plugins/plugin_edit_ctrl.ts +++ b/public/app/features/plugins/plugin_edit_ctrl.ts @@ -97,28 +97,7 @@ export class PluginEditCtrl { } importDashboards() { - // move to dashboards tab - this.tabIndex = 2; - - return new Promise((resolve) => { - if (!this.$scope.$$phase) { - this.$scope.$digest(); - } - - // let angular load dashboards tab - setTimeout(() => { - resolve(); - }, 1000); - - }).then(() => { - return new Promise((resolve, reject) => { - // send event to import list component - appEvents.emit('dashboard-list-import-all', { - resolve: resolve, - reject: reject - }); - }); - }); + return Promise.resolve(); } setPreUpdateHook(callback: () => any) { diff --git a/public/app/partials/confirm_modal.html b/public/app/partials/confirm_modal.html index d9da8bdacc4..6ab4a6b32c5 100644 --- a/public/app/partials/confirm_modal.html +++ b/public/app/partials/confirm_modal.html @@ -1,4 +1,4 @@ -
  • +
    @@ -152,7 +157,7 @@ Decimals
  • - +
  • diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index d7e6f7918a2..86847a01aac 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -45,7 +45,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { }; /** @ngInject */ - constructor($scope, $injector, private annotationsSrv) { + constructor($scope, $injector, private annotationsSrv, private $sanitize) { super($scope, $injector); this.pageIndex = 0; @@ -139,7 +139,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { } exportCsv() { - var renderer = new TableRenderer(this.panel, this.table, this.dashboard.isTimezoneUtc()); + var renderer = new TableRenderer(this.panel, this.table, this.dashboard.isTimezoneUtc(), this.$sanitize); FileExport.exportTableDataToCsv(renderer.render_values()); } @@ -160,7 +160,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { } function appendTableRows(tbodyElem) { - var renderer = new TableRenderer(panel, data, ctrl.dashboard.isTimezoneUtc()); + var renderer = new TableRenderer(panel, data, ctrl.dashboard.isTimezoneUtc(), ctrl.$sanitize); tbodyElem.empty(); tbodyElem.html(renderer.render(ctrl.pageIndex)); } diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index 0eb65916fc7..819471c9470 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -8,7 +8,7 @@ export class TableRenderer { formaters: any[]; colorState: any; - constructor(private panel, private table, private isUtc) { + constructor(private panel, private table, private isUtc, private sanitize) { this.formaters = []; this.colorState = {}; } @@ -24,7 +24,7 @@ export class TableRenderer { return _.first(style.colors); } - defaultCellFormater(v) { + defaultCellFormater(v, style) { if (v === null || v === void 0 || v === undefined) { return ''; } @@ -33,7 +33,11 @@ export class TableRenderer { v = v.join(', '); } - return v; + if (style && style.sanitize) { + return this.sanitize(v); + } else { + return _.escape(v); + } } createColumnFormater(style, column) { @@ -61,7 +65,7 @@ export class TableRenderer { } if (_.isString(v)) { - return v; + return this.defaultCellFormater(v, style); } if (style.colorMode) { @@ -72,7 +76,9 @@ export class TableRenderer { }; } - return this.defaultCellFormater; + return (value) => { + return this.defaultCellFormater(value, style); + }; } formatColumnValue(colIndex, value) { @@ -96,7 +102,6 @@ export class TableRenderer { renderCell(columnIndex, value, addWidthHack = false) { value = this.formatColumnValue(columnIndex, value); - value = _.escape(value); var style = ''; if (this.colorState.cell) { style = ' style="background-color:' + this.colorState.cell + ';color: white"'; diff --git a/public/app/plugins/panel/table/specs/renderer_specs.ts b/public/app/plugins/panel/table/specs/renderer_specs.ts index 638d31e5a95..9cf2ba1046b 100644 --- a/public/app/plugins/panel/table/specs/renderer_specs.ts +++ b/public/app/plugins/panel/table/specs/renderer_specs.ts @@ -13,6 +13,7 @@ describe('when rendering table', () => { {text: 'Undefined'}, {text: 'String'}, {text: 'United', unit: 'bps'}, + {text: 'Sanitized'}, ]; var panel = { @@ -47,11 +48,20 @@ describe('when rendering table', () => { type: 'number', unit: 'ms', decimals: 2, + }, + { + pattern: 'Sanitized', + type: 'string', + sanitize: true, } ] }; - var renderer = new TableRenderer(panel, table, 'utc'); + var sanitize = function(value) { + return 'sanitized'; + }; + + var renderer = new TableRenderer(panel, table, 'utc', sanitize); it('time column should be formated', () => { var html = renderer.renderCell(0, 1388556366666); @@ -107,6 +117,11 @@ describe('when rendering table', () => { var html = renderer.renderCell(3, undefined); expect(html).to.be(''); }); + + it('sanitized value should render as', () => { + var html = renderer.renderCell(6, 'text link'); + expect(html).to.be('sanitized'); + }); }); }); diff --git a/public/img/grafana_net_logo.svg b/public/img/grafana_net_logo.svg new file mode 100644 index 00000000000..d73b9b944a8 --- /dev/null +++ b/public/img/grafana_net_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/sass/components/_modals.scss b/public/sass/components/_modals.scss index 131e2b6364c..40cab02eac8 100644 --- a/public/sass/components/_modals.scss +++ b/public/sass/components/_modals.scss @@ -105,7 +105,7 @@ } .confirm-modal-text2 { - font-size: $font-size-h5; + font-size: $font-size-root; padding-top: $spacer; } diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 8824c827562..563f89919e5 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -101,8 +101,24 @@ .search-button-row { padding-top: 20px; + button, a { margin-right: 10px; } + + .search-button-row-explore-link { + color: $text-muted; + font-size: $font-size-sm; + padding-right: 7rem; + background: url(../img/grafana_net_logo.svg); + background-size: 6.5rem 3rem; + background-repeat: no-repeat; + background-position: right; + position: relative; + top: 0.8rem; + &:hover { + color: $link-hover-color; + } + } } diff --git a/public/sass/pages/_plugins.scss b/public/sass/pages/_plugins.scss index 970d048cce3..ed4d39d316f 100644 --- a/public/sass/pages/_plugins.scss +++ b/public/sass/pages/_plugins.scss @@ -63,28 +63,23 @@ } } -// .app-edit-logo-box { -// padding: 1.2rem; -// background: $panel-bg; -// text-align: center; -// img { -// max-width: 7rem; -// } -// margin-right: 2rem; -// } -// -// .app-edit-links { -// list-style: none; -// margin: 0 0 0 2rem; -// -// li { -// background: $panel-bg; -// margin-top: 4px; -// padding: 0.2rem 1rem; -// } -// } -// -// .app-edit-description { -// font-style: italic; -// margin-bottom: 1.5rem; -// } +.get-more-plugins-link { + color: $text-muted; + font-size: $font-size-sm; + padding-right: 7rem; + background: url(../img/grafana_net_logo.svg); + background-size: 6.5rem 3rem; + background-repeat: no-repeat; + background-position: right; + position: relative; + top: 1.2rem; + &:hover { + color: $link-hover-color; + } +} + +@include media-breakpoint-down(sm) { + .get-more-plugins-link { + display: none; + } +} diff --git a/public/test/core/utils/kbn_specs.js b/public/test/core/utils/kbn_specs.js index 2c828235c55..e851e9b82f5 100644 --- a/public/test/core/utils/kbn_specs.js +++ b/public/test/core/utils/kbn_specs.js @@ -161,4 +161,50 @@ define([ expect(str).to.be('15ms'); }); }); + + describe('hex', function() { + it('positive integer', function() { + var str = kbn.valueFormats.hex(100, 0); + expect(str).to.be('64'); + }); + it('negative integer', function() { + var str = kbn.valueFormats.hex(-100, 0); + expect(str).to.be('-64'); + }); + it('null', function() { + var str = kbn.valueFormats.hex(null, 0); + expect(str).to.be(''); + }); + it('positive float', function() { + var str = kbn.valueFormats.hex(50.52, 1); + expect(str).to.be('32.8'); + }); + it('negative float', function() { + var str = kbn.valueFormats.hex(-50.333, 2); + expect(str).to.be('-32.547AE147AE14'); + }); + }); + + describe('hex 0x', function() { + it('positive integeter', function() { + var str = kbn.valueFormats.hex0x(7999,0); + expect(str).to.be('0x1F3F'); + }); + it('negative integer', function() { + var str = kbn.valueFormats.hex0x(-584,0); + expect(str).to.be('-0x248'); + }); + it('null', function() { + var str = kbn.valueFormats.hex0x(null, 0); + expect(str).to.be(''); + }); + it('positive float', function() { + var str = kbn.valueFormats.hex0x(74.443, 3); + expect(str).to.be('0x4A.716872B020C4'); + }); + it('negative float', function() { + var str = kbn.valueFormats.hex0x(-65.458, 1); + expect(str).to.be('-0x41.8'); + }); + }); }); diff --git a/public/vendor/flot/jquery.flot.selection.js b/public/vendor/flot/jquery.flot.selection.js index f8fa668ff4f..b7993d92ee3 100644 --- a/public/vendor/flot/jquery.flot.selection.js +++ b/public/vendor/flot/jquery.flot.selection.js @@ -94,11 +94,11 @@ The plugin allso adds the following methods to the plot object: var savedhandlers = {}; var mouseUpHandler = null; - + function onMouseMove(e) { if (selection.active) { updateSelection(e); - + plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); } } @@ -106,7 +106,7 @@ The plugin allso adds the following methods to the plot object: function onMouseDown(e) { if (e.which != 1) // only accept left-click return; - + // cancel out any text selections document.body.focus(); @@ -127,13 +127,13 @@ The plugin allso adds the following methods to the plot object: // this is a bit silly, but we have to use a closure to be // able to whack the same handler again mouseUpHandler = function (e) { onMouseUp(e); }; - + $(document).one("mouseup", mouseUpHandler); } function onMouseUp(e) { mouseUpHandler = null; - + // revert drag stuff for old-school browsers if (document.onselectstart !== undefined) document.onselectstart = savedhandlers.onselectstart; @@ -158,13 +158,22 @@ The plugin allso adds the following methods to the plot object: function getSelection() { if (!selectionIsSane()) return null; - + if (!selection.show) return null; var r = {}, c1 = selection.first, c2 = selection.second; - $.each(plot.getAxes(), function (name, axis) { - if (axis.used) { - var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); + var axes = plot.getAxes(); + // look if no axis is used + var noAxisInUse = true; + $.each(axes, function (name, axis) { + if (axis.used) { + anyUsed = false; + } + }) + + $.each(axes, function (name, axis) { + if (axis.used || noAxisInUse) { + var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; } }); @@ -252,10 +261,10 @@ The plugin allso adds the following methods to the plot object: from = to; to = tmp; } - + return { from: from, to: to, axis: axis }; } - + function setSelection(ranges, preventEvent) { var axis, range, o = plot.getOptions(); @@ -333,11 +342,11 @@ The plugin allso adds the following methods to the plot object: ctx.restore(); } }); - + plot.hooks.shutdown.push(function (plot, eventHolder) { eventHolder.unbind("mousemove", onMouseMove); eventHolder.unbind("mousedown", onMouseDown); - + if (mouseUpHandler) $(document).unbind("mouseup", mouseUpHandler); }); diff --git a/tests/test-app/dashboards/connections.json b/tests/test-app/dashboards/connections.json index ee5e40140b3..cc189d2d113 100644 --- a/tests/test-app/dashboards/connections.json +++ b/tests/test-app/dashboards/connections.json @@ -8,7 +8,7 @@ ], "title": "Nginx Connections", - "revision": "1.5", + "revision": 25, "schemaVersion": 11, "tags": ["tag1", "tag2"], "number_array": [1,2,3,10.33], diff --git a/tests/test-app/dashboards/connections_result.json b/tests/test-app/dashboards/connections_result.json index 1c662f6c269..4bf0570ac1e 100644 --- a/tests/test-app/dashboards/connections_result.json +++ b/tests/test-app/dashboards/connections_result.json @@ -1,5 +1,5 @@ { - "revision": "1.5", + "revision": 25, "tags": ["tag1", "tag2"], "boolean_false": false, "boolean_true": true, diff --git a/tests/test-app/dashboards/memory.json b/tests/test-app/dashboards/memory.json index b79cb4d7dba..983b994b40e 100644 --- a/tests/test-app/dashboards/memory.json +++ b/tests/test-app/dashboards/memory.json @@ -1,5 +1,5 @@ { "title": "Nginx Memory", - "revision": "2.0", + "revision": 2, "schemaVersion": 11 }