From 2bd2605ae96d6ec2f60b2d21305fd03ecc52ff02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 20 Mar 2015 19:16:59 -0400 Subject: [PATCH 01/64] Added poc of dashboard snapshot, sharable dashboard with data embedded --- src/app/features/dashboard/dashboardNavCtrl.js | 14 +++++++++++++- src/app/panels/graph/module.js | 12 +++++++++++- src/app/partials/dashboard_topnav.html | 1 + 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/app/features/dashboard/dashboardNavCtrl.js b/src/app/features/dashboard/dashboardNavCtrl.js index a47e19b561f..9a8042cd680 100644 --- a/src/app/features/dashboard/dashboardNavCtrl.js +++ b/src/app/features/dashboard/dashboardNavCtrl.js @@ -11,7 +11,7 @@ function (angular, _, moment) { var module = angular.module('grafana.controllers'); - module.controller('DashboardNavCtrl', function($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, timeSrv) { + module.controller('DashboardNavCtrl', function($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, timeSrv, $timeout) { $scope.init = function() { $scope.onAppEvent('save-dashboard', $scope.saveDashboard); @@ -157,6 +157,18 @@ function (angular, _, moment) { }); }; + $scope.snapshot = function() { + $scope.dashboard.snapshot = true; + $rootScope.$broadcast('refresh'); + + $timeout(function() { + $scope.exportDashboard(); + $scope.dashboard.snapshot = false; + $scope.appEvent('dashboard-snapshot-cleanup'); + }, 1000); + + }; + $scope.editJson = function() { $scope.appEvent('show-json-editor', { object: $scope.dashboard }); }; diff --git a/src/app/panels/graph/module.js b/src/app/panels/graph/module.js index 3af9f59eb9b..966e1f9aa23 100644 --- a/src/app/panels/graph/module.js +++ b/src/app/panels/graph/module.js @@ -23,7 +23,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { }; }); - module.controller('GraphCtrl', function($scope, $rootScope, panelSrv, annotationsSrv, panelHelper) { + module.controller('GraphCtrl', function($scope, $rootScope, panelSrv, annotationsSrv, panelHelper, $q) { $scope.panelMeta = new PanelMeta({ panelName: 'Graph', @@ -130,6 +130,12 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { $scope.refreshData = function(datasource) { panelHelper.updateTimeRange($scope); + if ($scope.panel.snapshotData) { + $scope.annotationsPromise = $q.when([]); + $scope.dataHandler($scope.panel.snapshotData); + return; + } + $scope.annotationsPromise = annotationsSrv.getAnnotations($scope.rangeUnparsed, $scope.dashboard); return panelHelper.issueMetricQuery($scope, datasource) @@ -141,6 +147,9 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { }; $scope.dataHandler = function(results) { + if ($scope.dashboard.snapshot) { + $scope.panel.snapshotData = results; + } // png renderer returns just a url if (_.isString(results)) { $scope.render(results); @@ -285,6 +294,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { }; panelSrv.init($scope); + }); }); diff --git a/src/app/partials/dashboard_topnav.html b/src/app/partials/dashboard_topnav.html index bf3ba635a97..77cccecb10b 100644 --- a/src/app/partials/dashboard_topnav.html +++ b/src/app/partials/dashboard_topnav.html @@ -40,6 +40,7 @@
  • View JSON
  • Save As...
  • Delete dashboard
  • +
  • Snapshot dashboard
  • From 7db37032759427c5a9b36654933982b7582a3caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 20 Mar 2015 22:01:39 -0400 Subject: [PATCH 02/64] Share Panel: The share modal now has an embed option, gives you an iframe that you can use to embedd a single graph on another web site, #1622 --- CHANGELOG.md | 1 + .../features/dashboard/dashboardNavCtrl.js | 2 +- .../dashboard/partials/shareDashboard.html | 48 ++++++++++++++ .../dashboard/partials/shareModal.html | 53 --------------- .../dashboard/partials/sharePanel.html | 65 +++++++++++++++++++ src/app/features/dashboard/sharePanelCtrl.js | 5 +- src/app/features/panel/panelSrv.js | 2 +- src/app/features/panel/soloPanelCtrl.js | 16 ++++- src/css/less/forms.less | 6 ++ src/test/specs/soloPanelCtrl-specs.js | 6 ++ 10 files changed, 146 insertions(+), 58 deletions(-) create mode 100644 src/app/features/dashboard/partials/shareDashboard.html delete mode 100644 src/app/features/dashboard/partials/shareModal.html create mode 100644 src/app/features/dashboard/partials/sharePanel.html diff --git a/CHANGELOG.md b/CHANGELOG.md index eb1ffcdecb9..15e1527ad8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 2.0.0 (unreleased) **New features** +- [Issue #1622](https://github.com/grafana/grafana/issues/1622). Share Panel: The share modal now has an embed option, gives you an iframe that you can use to embedd a single graph on another web site - [Issue #718](https://github.com/grafana/grafana/issues/718). Dashboard: When saving a dashboard and another user has made changes inbetween the user is promted with a warning if he really wants to overwrite the other's changes - [Issue #1331](https://github.com/grafana/grafana/issues/1331). Graph & Singlestat: New axis/unit format selector and more units (kbytes, Joule, Watt, eV), and new design for graph axis & grid tab and single stat options tab views - [Issue #1241](https://github.com/grafana/grafana/issues/1242). Timepicker: New option in timepicker (under dashboard settings), to change ``now`` to be for example ``now-1m``, usefull when you want to ignore last minute because it contains incomplete data diff --git a/src/app/features/dashboard/dashboardNavCtrl.js b/src/app/features/dashboard/dashboardNavCtrl.js index a47e19b561f..5950c504a3a 100644 --- a/src/app/features/dashboard/dashboardNavCtrl.js +++ b/src/app/features/dashboard/dashboardNavCtrl.js @@ -42,7 +42,7 @@ function (angular, _, moment) { $scope.shareDashboard = function() { $scope.appEvent('show-modal', { - src: './app/features/dashboard/partials/shareModal.html', + src: './app/features/dashboard/partials/shareDashboard.html', scope: $scope.$new(), }); }; diff --git a/src/app/features/dashboard/partials/shareDashboard.html b/src/app/features/dashboard/partials/shareDashboard.html new file mode 100644 index 00000000000..e052c5b298f --- /dev/null +++ b/src/app/features/dashboard/partials/shareDashboard.html @@ -0,0 +1,48 @@ + diff --git a/src/app/features/dashboard/partials/shareModal.html b/src/app/features/dashboard/partials/shareModal.html deleted file mode 100644 index a9707be94b1..00000000000 --- a/src/app/features/dashboard/partials/shareModal.html +++ /dev/null @@ -1,53 +0,0 @@ - diff --git a/src/app/features/dashboard/sharePanelCtrl.js b/src/app/features/dashboard/sharePanelCtrl.js index 40c7ba45a90..88710660e3c 100644 --- a/src/app/features/dashboard/sharePanelCtrl.js +++ b/src/app/features/dashboard/sharePanelCtrl.js @@ -9,7 +9,7 @@ function (angular, _, require, config) { var module = angular.module('grafana.controllers'); - module.controller('SharePanelCtrl', function($scope, $location, $timeout, timeSrv, $element, templateSrv) { + module.controller('SharePanelCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, $element, templateSrv) { $scope.init = function() { $scope.editor = { index: 0 }; @@ -81,6 +81,17 @@ function (angular, _, require, config) { $scope.imageUrl += '&height=500'; }; + $scope.snapshot = function() { + $scope.dashboard.snapshot = true; + $rootScope.$broadcast('refresh'); + + $timeout(function() { + $scope.exportDashboard(); + $scope.dashboard.snapshot = false; + $scope.appEvent('dashboard-snapshot-cleanup'); + }, 1000); + }; + $scope.init(); }); diff --git a/src/app/features/dashboard/shareSnapshotCtrl.js b/src/app/features/dashboard/shareSnapshotCtrl.js new file mode 100644 index 00000000000..fcc4ce2ba1a --- /dev/null +++ b/src/app/features/dashboard/shareSnapshotCtrl.js @@ -0,0 +1,30 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('ShareSnapshotCtrl', function($scope, $rootScope, backendSrv, $timeout) { + + $scope.snapshot = function() { + $scope.dashboard.snapshot = true; + $rootScope.$broadcast('refresh'); + + $timeout(function() { + var dash = angular.copy($scope.dashboard); + backendSrv.post('/api/snapshots/', { + dashboard: dash + }).then(function(results) { + console.log(results); + }); + + $scope.dashboard.snapshot = false; + $scope.appEvent('dashboard-snapshot-cleanup'); + }, 2000); + }; + + }); + +}); diff --git a/src/app/partials/shareDashboard.html b/src/app/partials/shareDashboard.html deleted file mode 100644 index 79ff15c548e..00000000000 --- a/src/app/partials/shareDashboard.html +++ /dev/null @@ -1,18 +0,0 @@ - From f48f5428e57cb6c082e5ed875a41469164ef10ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 21 Mar 2015 10:56:26 -0400 Subject: [PATCH 04/64] Adding snapshot storage and route, #1623 --- pkg/api/dashboard_snapshot.go | 10 +++++-- pkg/api/dtos/models.go | 7 +++-- .../migrations/dashboard_snapshot_mig.go | 6 ++-- .../dashboard/partials/shareDashboard.html | 28 ++++++++++++++++++- .../features/dashboard/shareSnapshotCtrl.js | 21 ++++++++++---- src/app/routes/all.js | 4 +++ src/app/routes/dashLoadControllers.js | 23 ++++++++++----- 7 files changed, 77 insertions(+), 22 deletions(-) diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 3ac574d4991..979a3aa86f2 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -1,13 +1,14 @@ package api import ( + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" ) -func CreateDashboardSnapshotCommand(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) { +func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) { cmd.Key = util.GetRandomString(20) if err := bus.Dispatch(&cmd); err != nil { @@ -29,5 +30,10 @@ func GetDashboardSnapshot(c *middleware.Context) { return } - c.JSON(200, query.Result) + dto := dtos.Dashboard{ + Model: query.Result.Dashboard, + Meta: dtos.DashboardMeta{IsSnapshot: true}, + } + + c.JSON(200, dto) } diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index c225c6a5bbb..2cb9da0189f 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -27,9 +27,10 @@ type CurrentUser struct { } type DashboardMeta struct { - IsStarred bool `json:"isStarred"` - IsHome bool `json:"isHome"` - Slug string `json:"slug"` + IsStarred bool `json:"isStarred"` + IsHome bool `json:"isHome"` + IsSnapshot bool `json:"isSnapshot"` + Slug string `json:"slug"` } type Dashboard struct { diff --git a/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go b/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go index 8ac7bee8be4..a30b5eccbe4 100644 --- a/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go @@ -3,7 +3,7 @@ package migrations import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" func addDashboardSnapshotMigrations(mg *Migrator) { - snapshotV3 := Table{ + snapshotV4 := Table{ Name: "dashboard_snapshot", Columns: []*Column{ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, @@ -19,6 +19,6 @@ func addDashboardSnapshotMigrations(mg *Migrator) { }, } - mg.AddMigration("create dashboard_snapshot table v3", NewAddTableMigration(snapshotV3)) - addTableIndicesMigrations(mg, "v3", snapshotV3) + mg.AddMigration("create dashboard_snapshot table v4", NewAddTableMigration(snapshotV4)) + addTableIndicesMigrations(mg, "v4", snapshotV4) } diff --git a/src/app/features/dashboard/partials/shareDashboard.html b/src/app/features/dashboard/partials/shareDashboard.html index c622254292a..0463b342650 100644 --- a/src/app/features/dashboard/partials/shareDashboard.html +++ b/src/app/features/dashboard/partials/shareDashboard.html @@ -54,7 +54,33 @@

    - +
    +
    +
      +
    • + Snapshot name +
    • +
    • + +
    • +
    +
    +
    +
    + +
    +
    + + + + +
    +
    + + diff --git a/src/app/features/dashboard/shareSnapshotCtrl.js b/src/app/features/dashboard/shareSnapshotCtrl.js index fcc4ce2ba1a..2f316c71ada 100644 --- a/src/app/features/dashboard/shareSnapshotCtrl.js +++ b/src/app/features/dashboard/shareSnapshotCtrl.js @@ -6,18 +6,27 @@ function (angular) { var module = angular.module('grafana.controllers'); - module.controller('ShareSnapshotCtrl', function($scope, $rootScope, backendSrv, $timeout) { + module.controller('ShareSnapshotCtrl', function($scope, $rootScope, $location, backendSrv, $timeout) { - $scope.snapshot = function() { + $scope.snapshot = { + name: $scope.dashboard.title + }; + + $scope.createSnapshot = function() { $scope.dashboard.snapshot = true; + $scope.loading = true; $rootScope.$broadcast('refresh'); $timeout(function() { var dash = angular.copy($scope.dashboard); - backendSrv.post('/api/snapshots/', { - dashboard: dash - }).then(function(results) { - console.log(results); + backendSrv.post('/api/snapshots/', {dashboard: dash}).then(function(results) { + $scope.loading = false; + + var baseUrl = $location.absUrl().replace($location.url(), ""); + $scope.snapshotUrl = baseUrl + '/dashboard/snapshots/' + results.key; + + }, function() { + $scope.loading = false; }); $scope.dashboard.snapshot = false; diff --git a/src/app/routes/all.js b/src/app/routes/all.js index de6477ea959..d68fbbdd525 100644 --- a/src/app/routes/all.js +++ b/src/app/routes/all.js @@ -35,6 +35,10 @@ define([ controller : 'DashFromImportCtrl', reloadOnSearch: false, }) + .when('/dashboard/snapshots/:key', { + templateUrl: 'app/partials/dashboard.html', + controller : 'DashFromSnapshotCtrl', + }) .when('/dashboard/new', { templateUrl: 'app/partials/dashboard.html', controller : 'NewDashboardCtrl', diff --git a/src/app/routes/dashLoadControllers.js b/src/app/routes/dashLoadControllers.js index 710c30719cc..1a1d61142b2 100644 --- a/src/app/routes/dashLoadControllers.js +++ b/src/app/routes/dashLoadControllers.js @@ -33,6 +33,15 @@ function (angular, _, kbn, moment, $) { }); }); + module.controller('DashFromSnapshotCtrl', function($scope, $routeParams, backendSrv) { + backendSrv.get('/api/snapshots/' + $routeParams.key).then(function(result) { + $scope.initDashboard(result, $scope); + },function() { + $scope.initDashboard({}, $scope); + $scope.appEvent('alert-error', ['Dashboard Snapshot', '']); + }); + }); + module.controller('DashFromImportCtrl', function($scope, $location, alertSrv) { if (!window.grafanaImportDashboard) { alertSrv.set('Not found', 'Cannot reload page with unsaved imported dashboard', 'warning', 7000); @@ -47,7 +56,7 @@ function (angular, _, kbn, moment, $) { meta: {}, model: { title: "New dashboard", - rows: [{ height: '250px', panels:[] }] + rows: [{ height: '250px', panels:[] }] }, }, $scope); }); @@ -57,10 +66,10 @@ function (angular, _, kbn, moment, $) { var file_load = function(file) { return $http({ url: "public/dashboards/"+file.replace(/\.(?!json)/,"/")+'?' + new Date().getTime(), - method: "GET", - transformResponse: function(response) { - return angular.fromJson(response); - } + method: "GET", + transformResponse: function(response) { + return angular.fromJson(response); + } }).then(function(result) { if(!result) { return false; @@ -83,8 +92,8 @@ function (angular, _, kbn, moment, $) { var execute_script = function(result) { var services = { dashboardSrv: dashboardSrv, - datasourceSrv: datasourceSrv, - $q: $q, + datasourceSrv: datasourceSrv, + $q: $q, }; /*jshint -W054 */ From 7d4293f849d6c8d32e4ee05df809823e2ca3a722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 22 Mar 2015 06:48:53 -0400 Subject: [PATCH 05/64] removed cli commands, need to be mobed to a seperate binary using http api, #1570 --- main.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/main.go b/main.go index f23a544e1e9..3b25b73039d 100644 --- a/main.go +++ b/main.go @@ -39,17 +39,7 @@ func main() { app.Name = "Grafana Backend" app.Usage = "grafana web" app.Version = version - app.Commands = []cli.Command{ - cmd.ListOrgs, - cmd.CreateOrg, - cmd.DeleteOrg, - cmd.ExportDashboard, - cmd.ImportDashboard, - cmd.ListDataSources, - cmd.CreateDataSource, - cmd.DescribeDataSource, - cmd.DeleteDataSource, - cmd.Web} + app.Commands = []cli.Command{cmd.ImportDashboard, cmd.Web} app.Flags = append(app.Flags, []cli.Flag{ cli.StringFlag{ Name: "config", From d987532262435937215e72664101257058d2057e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 22 Mar 2015 15:14:00 -0400 Subject: [PATCH 06/64] Added server metrics --- conf/defaults.ini | 7 ++ pkg/api/admin_users.go | 3 + pkg/api/dashboard.go | 5 ++ pkg/api/index.go | 2 +- pkg/api/login.go | 4 +- pkg/api/login_oauth.go | 3 + pkg/api/org.go | 3 + pkg/api/signup.go | 3 + pkg/cmd/web.go | 5 ++ pkg/metrics/counter.go | 72 ++++++++++++++++++++ pkg/metrics/metric_ref.go | 39 +++++++++++ pkg/metrics/metrics.go | 25 +++++++ pkg/metrics/registry.go | 102 +++++++++++++++++++++++++++++ pkg/metrics/report_usage.go | 60 +++++++++++++++++ pkg/middleware/middleware.go | 12 ++++ pkg/models/stats.go | 11 ++++ pkg/services/sqlstore/dashboard.go | 2 + pkg/services/sqlstore/stats.go | 36 ++++++++++ pkg/setting/setting.go | 4 ++ 19 files changed, 396 insertions(+), 2 deletions(-) create mode 100644 pkg/metrics/counter.go create mode 100644 pkg/metrics/metric_ref.go create mode 100644 pkg/metrics/metrics.go create mode 100644 pkg/metrics/registry.go create mode 100644 pkg/metrics/report_usage.go create mode 100644 pkg/models/stats.go create mode 100644 pkg/services/sqlstore/stats.go diff --git a/conf/defaults.ini b/conf/defaults.ini index 5318e89b2ad..095457dd2a2 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1,6 +1,13 @@ app_name = Grafana app_mode = production +# Once every 24 hours Grafana will report anonymous data to +# stats.grafana.org (https). No ip addresses are being tracked. +# only simple counters to track running instances, dashboard +# count and errors. It is very helpful to us. +# Change this option to false to disable reporting. +reporting-enabled = true + [server] ; protocol (http or https) protocol = http diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index d3fa111d333..f7e8fca2b5e 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -3,6 +3,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" @@ -64,6 +65,8 @@ func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) { return } + metrics.M_Api_Admin_User_Create.Inc(1) + c.JsonOK("User created") } diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 8cde5a8bc8a..278264f22d7 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -27,6 +28,8 @@ func isDasboardStarredByUser(c *middleware.Context, dashId int64) (bool, error) } func GetDashboard(c *middleware.Context) { + metrics.M_Api_Dashboard_Get.Inc(1) + slug := c.Params(":slug") query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId} @@ -88,6 +91,8 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) { return } + metrics.M_Api_Dashboard_Post.Inc(1) + c.JSON(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version}) } diff --git a/pkg/api/index.go b/pkg/api/index.go index 3006c54e8ab..4af66f18133 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -47,7 +47,7 @@ func Index(c *middleware.Context) { func NotFound(c *middleware.Context) { if c.IsApiRequest() { - c.JsonApiErr(200, "Not found", nil) + c.JsonApiErr(404, "Not found", nil) return } diff --git a/pkg/api/login.go b/pkg/api/login.go index e7707c53138..56a61697cb9 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -75,7 +76,6 @@ func LoginView(c *middleware.Context) { } func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) { - userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.User} err := bus.Dispatch(&userQuery) @@ -112,6 +112,8 @@ func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) { c.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/") } + metrics.M_Api_Login_Post.Inc(1) + c.JSON(200, result) } diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 9ccb8f0b60d..a234ef02bf3 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -81,5 +82,7 @@ func OAuthLogin(ctx *middleware.Context) { // login loginUserWithUser(userQuery.Result, ctx) + metrics.M_Api_Login_OAuth.Inc(1) + ctx.Redirect(setting.AppSubUrl + "/") } diff --git a/pkg/api/org.go b/pkg/api/org.go index 8b41b0e3f5f..ed180b1af77 100644 --- a/pkg/api/org.go +++ b/pkg/api/org.go @@ -2,6 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" ) @@ -35,6 +36,8 @@ func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) { return } + metrics.M_Api_Org_Create.Inc(1) + c.JsonOK("Organization created") } diff --git a/pkg/api/signup.go b/pkg/api/signup.go index 74f00509b98..63bb34c72ac 100644 --- a/pkg/api/signup.go +++ b/pkg/api/signup.go @@ -2,6 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -26,4 +27,6 @@ func SignUp(c *middleware.Context, cmd m.CreateUserCommand) { loginUserWithUser(&user, c) c.JsonOK("User created and logged in") + + metrics.M_Api_User_SignUp.Inc(1) } diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index 6619e5b1e0e..e5516fb52d9 100644 --- a/pkg/cmd/web.go +++ b/pkg/cmd/web.go @@ -21,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/eventpublisher" @@ -88,6 +89,10 @@ func runWeb(c *cli.Context) { m := newMacaron() api.Register(m) + if setting.ReportingEnabled { + go metrics.StartUsageReportLoop() + } + listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort) log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubUrl) switch setting.Protocol { diff --git a/pkg/metrics/counter.go b/pkg/metrics/counter.go new file mode 100644 index 00000000000..1a4a88be37b --- /dev/null +++ b/pkg/metrics/counter.go @@ -0,0 +1,72 @@ +package metrics + +import "sync/atomic" + +// Counters hold an int64 value that can be incremented and decremented. +type Counter interface { + Clear() + Count() int64 + Dec(int64) + Inc(int64) + Snapshot() Counter +} + +// NewCounter constructs a new StandardCounter. +func NewCounter() Counter { + return &StandardCounter{0} +} + +// CounterSnapshot is a read-only copy of another Counter. +type CounterSnapshot int64 + +// Clear panics. +func (CounterSnapshot) Clear() { + panic("Clear called on a CounterSnapshot") +} + +// Count returns the count at the time the snapshot was taken. +func (c CounterSnapshot) Count() int64 { return int64(c) } + +// Dec panics. +func (CounterSnapshot) Dec(int64) { + panic("Dec called on a CounterSnapshot") +} + +// Inc panics. +func (CounterSnapshot) Inc(int64) { + panic("Inc called on a CounterSnapshot") +} + +// Snapshot returns the snapshot. +func (c CounterSnapshot) Snapshot() Counter { return c } + +// StandardCounter is the standard implementation of a Counter and uses the +// sync/atomic package to manage a single int64 value. +type StandardCounter struct { + count int64 +} + +// Clear sets the counter to zero. +func (c *StandardCounter) Clear() { + atomic.StoreInt64(&c.count, 0) +} + +// Count returns the current count. +func (c *StandardCounter) Count() int64 { + return atomic.LoadInt64(&c.count) +} + +// Dec decrements the counter by the given amount. +func (c *StandardCounter) Dec(i int64) { + atomic.AddInt64(&c.count, -i) +} + +// Inc increments the counter by the given amount. +func (c *StandardCounter) Inc(i int64) { + atomic.AddInt64(&c.count, i) +} + +// Snapshot returns a read-only copy of the counter. +func (c *StandardCounter) Snapshot() Counter { + return CounterSnapshot(c.Count()) +} diff --git a/pkg/metrics/metric_ref.go b/pkg/metrics/metric_ref.go new file mode 100644 index 00000000000..f9e5d693d4c --- /dev/null +++ b/pkg/metrics/metric_ref.go @@ -0,0 +1,39 @@ +package metrics + +type comboCounterRef struct { + usageCounter Counter + metricCounter Counter +} + +func NewComboCounterRef(name string) Counter { + cr := &comboCounterRef{} + cr.usageCounter = UsageStats.GetOrRegister(name, NewCounter).(Counter) + cr.metricCounter = MetricStats.GetOrRegister(name, NewCounter).(Counter) + return cr +} + +func (c comboCounterRef) Clear() { + c.usageCounter.Clear() + c.metricCounter.Clear() +} + +func (c comboCounterRef) Count() int64 { + panic("Count called on a combocounter ref") +} + +// Dec panics. +func (c comboCounterRef) Dec(i int64) { + c.usageCounter.Dec(i) + c.metricCounter.Dec(i) +} + +// Inc panics. +func (c comboCounterRef) Inc(i int64) { + c.usageCounter.Inc(i) + c.metricCounter.Inc(i) +} + +// Snapshot returns the snapshot. +func (c comboCounterRef) Snapshot() Counter { + panic("snapshot called on a combocounter ref") +} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go new file mode 100644 index 00000000000..45b964fb56e --- /dev/null +++ b/pkg/metrics/metrics.go @@ -0,0 +1,25 @@ +package metrics + +var UsageStats = NewRegistry() +var MetricStats = NewRegistry() + +var ( + M_Instance_Start = NewComboCounterRef("instance.start") + + M_Page_Status_200 = NewComboCounterRef("page.status.200") + M_Page_Status_500 = NewComboCounterRef("page.status.500") + M_Page_Status_404 = NewComboCounterRef("page.status.404") + + M_Api_Status_500 = NewComboCounterRef("api.status.500") + M_Api_Status_404 = NewComboCounterRef("api.status.404") + + M_Api_User_SignUp = NewComboCounterRef("api.user.signup") + M_Api_Dashboard_Get = NewComboCounterRef("api.dashboard.get") + M_Api_Dashboard_Post = NewComboCounterRef("api.dashboard.post") + M_Api_Admin_User_Create = NewComboCounterRef("api.admin.user_create") + M_Api_Login_Post = NewComboCounterRef("api.login.post") + M_Api_Login_OAuth = NewComboCounterRef("api.login.oauth") + M_Api_Org_Create = NewComboCounterRef("api.org.create") + + M_Models_Dashboard_Insert = NewComboCounterRef("models.dashboard.insert") +) diff --git a/pkg/metrics/registry.go b/pkg/metrics/registry.go new file mode 100644 index 00000000000..9e1618f3691 --- /dev/null +++ b/pkg/metrics/registry.go @@ -0,0 +1,102 @@ +package metrics + +import ( + "fmt" + "reflect" + "sync" +) + +// DuplicateMetric is the error returned by Registry.Register when a metric +// already exists. If you mean to Register that metric you must first +// Unregister the existing metric. +type DuplicateMetric string + +func (err DuplicateMetric) Error() string { + return fmt.Sprintf("duplicate metric: %s", string(err)) +} + +type Registry interface { + // Call the given function for each registered metric. + Each(func(string, interface{})) + + // Get the metric by the given name or nil if none is registered. + Get(string) interface{} + + // Gets an existing metric or registers the given one. + // The interface can be the metric to register if not found in registry, + // or a function returning the metric for lazy instantiation. + GetOrRegister(string, interface{}) interface{} + + // Register the given metric under the given name. + Register(string, interface{}) error +} + +// The standard implementation of a Registry is a mutex-protected map +// of names to metrics. +type StandardRegistry struct { + metrics map[string]interface{} + mutex sync.Mutex +} + +// Create a new registry. +func NewRegistry() Registry { + return &StandardRegistry{metrics: make(map[string]interface{})} +} + +// Call the given function for each registered metric. +func (r *StandardRegistry) Each(f func(string, interface{})) { + for name, i := range r.registered() { + f(name, i) + } +} + +// Get the metric by the given name or nil if none is registered. +func (r *StandardRegistry) Get(name string) interface{} { + r.mutex.Lock() + defer r.mutex.Unlock() + return r.metrics[name] +} + +// Gets an existing metric or creates and registers a new one. Threadsafe +// alternative to calling Get and Register on failure. +// The interface can be the metric to register if not found in registry, +// or a function returning the metric for lazy instantiation. +func (r *StandardRegistry) GetOrRegister(name string, i interface{}) interface{} { + r.mutex.Lock() + defer r.mutex.Unlock() + if metric, ok := r.metrics[name]; ok { + return metric + } + if v := reflect.ValueOf(i); v.Kind() == reflect.Func { + i = v.Call(nil)[0].Interface() + } + r.register(name, i) + return i +} + +// Register the given metric under the given name. Returns a DuplicateMetric +// if a metric by the given name is already registered. +func (r *StandardRegistry) Register(name string, i interface{}) error { + r.mutex.Lock() + defer r.mutex.Unlock() + return r.register(name, i) +} + +func (r *StandardRegistry) register(name string, i interface{}) error { + if _, ok := r.metrics[name]; ok { + return DuplicateMetric(name) + } + + r.metrics[name] = i + return nil +} + +func (r *StandardRegistry) registered() map[string]interface{} { + metrics := make(map[string]interface{}, len(r.metrics)) + r.mutex.Lock() + defer r.mutex.Unlock() + for name, i := range r.metrics { + metrics[name] = i + } + return metrics +} diff --git a/pkg/metrics/report_usage.go b/pkg/metrics/report_usage.go new file mode 100644 index 00000000000..af6552b5c1e --- /dev/null +++ b/pkg/metrics/report_usage.go @@ -0,0 +1,60 @@ +package metrics + +import ( + "bytes" + "encoding/json" + "net/http" + "time" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/setting" +) + +func StartUsageReportLoop() chan struct{} { + M_Instance_Start.Inc(1) + + ticker := time.NewTicker(10 * time.Minute) + for { + select { + case <-ticker.C: + sendUsageStats() + } + } +} + +func sendUsageStats() { + log.Trace("Sending anonymous usage stats to stats.grafana.org") + + metrics := map[string]interface{}{} + report := map[string]interface{}{ + "version": setting.BuildVersion, + "metrics": metrics, + } + + // statsQuery := m.GetSystemStatsQuery{} + // if err := bus.Dispatch(&statsQuery); err != nil { + // log.Error(3, "Failed to get system stats", err) + // return + // } + + UsageStats.Each(func(name string, i interface{}) { + switch metric := i.(type) { + case Counter: + if metric.Count() > 0 { + metrics[name+".count"] = metric.Count() + metric.Clear() + } + } + }) + + // metrics["stats.dashboards.count"] = statsQuery.Result.DashboardCount + // metrics["stats.users.count"] = statsQuery.Result.UserCount + // metrics["stats.orgs.count"] = statsQuery.Result.OrgCount + + out, _ := json.Marshal(report) + data := bytes.NewBuffer(out) + + client := http.Client{Timeout: time.Duration(5 * time.Second)} + + go client.Post("http://stats.grafana.org/grafana-usage-report", "application/json", data) +} diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index a15fd075fca..20e1eb196e5 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) @@ -99,6 +100,15 @@ func (ctx *Context) Handle(status int, title string, err error) { } } + switch status { + case 200: + metrics.M_Page_Status_200.Inc(1) + case 404: + metrics.M_Page_Status_404.Inc(1) + case 500: + metrics.M_Page_Status_500.Inc(1) + } + ctx.Data["Title"] = title ctx.HTML(status, strconv.Itoa(status)) } @@ -128,7 +138,9 @@ func (ctx *Context) JsonApiErr(status int, message string, err error) { switch status { case 404: resp["message"] = "Not Found" + metrics.M_Api_Status_500.Inc(1) case 500: + metrics.M_Api_Status_404.Inc(1) resp["message"] = "Internal Server Error" } diff --git a/pkg/models/stats.go b/pkg/models/stats.go new file mode 100644 index 00000000000..0d83882e666 --- /dev/null +++ b/pkg/models/stats.go @@ -0,0 +1,11 @@ +package models + +type SystemStats struct { + DashboardCount int + UserCount int + OrgCount int +} + +type GetSystemStatsQuery struct { + Result *SystemStats +} diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index bf748b600f4..0384a5bb6e6 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -6,6 +6,7 @@ import ( "github.com/go-xorm/xorm" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/metrics" m "github.com/grafana/grafana/pkg/models" ) @@ -48,6 +49,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { } if dash.Id == 0 { + metrics.M_Models_Dashboard_Insert.Inc(1) _, err = sess.Insert(dash) } else { dash.Version += 1 diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go new file mode 100644 index 00000000000..7995dd43f38 --- /dev/null +++ b/pkg/services/sqlstore/stats.go @@ -0,0 +1,36 @@ +package sqlstore + +import ( + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +func init() { + bus.AddHandler("sql", GetSystemStats) +} + +func GetSystemStats(query *m.GetSystemStatsQuery) error { + var rawSql = `SELECT + ( + SELECT COUNT(*) + FROM ` + dialect.Quote("user") + ` + ) AS user_count, + ( + SELECT COUNT(*) + FROM ` + dialect.Quote("org") + ` + ) AS org_count, + ( + SELECT COUNT(*) + FROM ` + dialect.Quote("dashboard") + ` + ) AS dashboard_count + ` + + var stats m.SystemStats + _, err := x.Sql(rawSql).Get(&stats) + if err != nil { + return err + } + + query.Result = &stats + return err +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index b8d038dbc29..defa8311e8c 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -96,6 +96,8 @@ var ( PhantomDir string configFiles []string + + ReportingEnabled bool ) func init() { @@ -233,6 +235,8 @@ func NewConfigContext(config string) { ImagesDir = "data/png" PhantomDir = "vendor/phantomjs" + ReportingEnabled = Cfg.Section("").Key("reporting-enabled").MustBool(true) + readSessionConfig() } From 9c9ebb49875941805e9e2e71284dcb30afdd13a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 22 Mar 2015 15:24:35 -0400 Subject: [PATCH 07/64] Updated server stats --- pkg/metrics/report_usage.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pkg/metrics/report_usage.go b/pkg/metrics/report_usage.go index af6552b5c1e..1887f7f60a9 100644 --- a/pkg/metrics/report_usage.go +++ b/pkg/metrics/report_usage.go @@ -6,7 +6,6 @@ import ( "net/http" "time" - "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/setting" ) @@ -23,8 +22,6 @@ func StartUsageReportLoop() chan struct{} { } func sendUsageStats() { - log.Trace("Sending anonymous usage stats to stats.grafana.org") - metrics := map[string]interface{}{} report := map[string]interface{}{ "version": setting.BuildVersion, @@ -56,5 +53,5 @@ func sendUsageStats() { client := http.Client{Timeout: time.Duration(5 * time.Second)} - go client.Post("http://stats.grafana.org/grafana-usage-report", "application/json", data) + go client.Post("https://stats.grafana.org/grafana-usage-report", "application/json", data) } From c67291da33a1f3a9ecbd54c3f54e2e3b24e864ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 22 Mar 2015 15:25:21 -0400 Subject: [PATCH 08/64] Updated --- conf/sample.ini | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/conf/sample.ini b/conf/sample.ini index 4e4c335ae18..a51600a606d 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -5,6 +5,13 @@ app_mode = production +# Once every 24 hours Grafana will report anonymous data to +# stats.grafana.org (https). No ip addresses are being tracked. +# only simple counters to track running instances, dashboard +# count and errors. It is very helpful to us. +# Change this option to false to disable reporting. +reporting-enabled = true + [server] ; protocol (http or https) protocol = http From 526f3e1a314248430fa7e18ae94fc71dd6822a62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 22 Mar 2015 15:27:05 -0400 Subject: [PATCH 09/64] Fixed failing unit test --- src/test/specs/graph-specs.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/specs/graph-specs.js b/src/test/specs/graph-specs.js index 7e870bfd7d0..a234323ee01 100644 --- a/src/test/specs/graph-specs.js +++ b/src/test/specs/graph-specs.js @@ -153,9 +153,9 @@ define([ it('should apply axis transform and ticks', function() { var axis = ctx.plotOptions.yaxes[0]; - expect(axis.transform(100)).to.be(Math.log(100+0.0001)); - expect(axis.ticks[0]).to.be(1); - expect(axis.ticks[1]).to.be(10); + expect(axis.transform(100)).to.be(Math.log(100+0.1)); + expect(axis.ticks[0]).to.be(0); + expect(axis.ticks[1]).to.be(1); }); }); From 1e4c62a70d12f3cc9027691cee9dc66b76d5b540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 22 Mar 2015 15:45:13 -0400 Subject: [PATCH 10/64] updated server reporting --- pkg/metrics/report_usage.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/metrics/report_usage.go b/pkg/metrics/report_usage.go index 1887f7f60a9..e5cbf7a45f1 100644 --- a/pkg/metrics/report_usage.go +++ b/pkg/metrics/report_usage.go @@ -6,13 +6,14 @@ import ( "net/http" "time" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/setting" ) func StartUsageReportLoop() chan struct{} { M_Instance_Start.Inc(1) - ticker := time.NewTicker(10 * time.Minute) + ticker := time.NewTicker(24 * time.Hour) for { select { case <-ticker.C: @@ -22,6 +23,8 @@ func StartUsageReportLoop() chan struct{} { } func sendUsageStats() { + log.Trace("Sending anonymous usage stats to stats.grafana.org") + metrics := map[string]interface{}{} report := map[string]interface{}{ "version": setting.BuildVersion, From a26436f59bae959087fba7c506e4312cb47aec1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 22 Mar 2015 16:13:16 -0400 Subject: [PATCH 11/64] Server metrics fix --- pkg/metrics/report_usage.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/metrics/report_usage.go b/pkg/metrics/report_usage.go index e5cbf7a45f1..b31b55333cc 100644 --- a/pkg/metrics/report_usage.go +++ b/pkg/metrics/report_usage.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "net/http" + "strings" "time" "github.com/grafana/grafana/pkg/log" @@ -13,7 +14,7 @@ import ( func StartUsageReportLoop() chan struct{} { M_Instance_Start.Inc(1) - ticker := time.NewTicker(24 * time.Hour) + ticker := time.NewTicker(10 * time.Minute) for { select { case <-ticker.C: @@ -25,9 +26,11 @@ func StartUsageReportLoop() chan struct{} { func sendUsageStats() { log.Trace("Sending anonymous usage stats to stats.grafana.org") + version := strings.Replace(setting.BuildVersion, ".", "_", -1) + metrics := map[string]interface{}{} report := map[string]interface{}{ - "version": setting.BuildVersion, + "version": version, "metrics": metrics, } From 44bc2b2d56423c3b25283bb3d0c4721d91441a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 22 Mar 2015 16:30:28 -0400 Subject: [PATCH 12/64] Updated conf description, metrics interval --- conf/defaults.ini | 2 +- conf/sample.ini | 4 ++-- pkg/metrics/report_usage.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 095457dd2a2..d35f71fa4ce 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1,7 +1,7 @@ app_name = Grafana app_mode = production -# Once every 24 hours Grafana will report anonymous data to +# Once every 1 hour Grafana will report anonymous data to # stats.grafana.org (https). No ip addresses are being tracked. # only simple counters to track running instances, dashboard # count and errors. It is very helpful to us. diff --git a/conf/sample.ini b/conf/sample.ini index a51600a606d..f7207668c8f 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -5,10 +5,10 @@ app_mode = production -# Once every 24 hours Grafana will report anonymous data to +# Once every 1 hour Grafana will report anonymous data to # stats.grafana.org (https). No ip addresses are being tracked. # only simple counters to track running instances, dashboard -# count and errors. It is very helpful to us. +# counts and errors. It is very helpful to us. # Change this option to false to disable reporting. reporting-enabled = true diff --git a/pkg/metrics/report_usage.go b/pkg/metrics/report_usage.go index b31b55333cc..f952e56fab6 100644 --- a/pkg/metrics/report_usage.go +++ b/pkg/metrics/report_usage.go @@ -14,7 +14,7 @@ import ( func StartUsageReportLoop() chan struct{} { M_Instance_Start.Inc(1) - ticker := time.NewTicker(10 * time.Minute) + ticker := time.NewTicker(time.Hour) for { select { case <-ticker.C: From a5fac17f2b7abbd8338922efbc891be21b30a251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 Mar 2015 07:32:03 -0400 Subject: [PATCH 13/64] Added public snapshot test, hosted on snapshots.raintank.io --- .../dashboard/partials/shareDashboard.html | 5 +++++ src/app/features/dashboard/shareSnapshotCtrl.js | 16 ++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/app/features/dashboard/partials/shareDashboard.html b/src/app/features/dashboard/partials/shareDashboard.html index f96fe844a89..1afd4a20a84 100644 --- a/src/app/features/dashboard/partials/shareDashboard.html +++ b/src/app/features/dashboard/partials/shareDashboard.html @@ -82,6 +82,11 @@ + + diff --git a/src/app/features/dashboard/shareSnapshotCtrl.js b/src/app/features/dashboard/shareSnapshotCtrl.js index 2f316c71ada..acc0f38d946 100644 --- a/src/app/features/dashboard/shareSnapshotCtrl.js +++ b/src/app/features/dashboard/shareSnapshotCtrl.js @@ -12,17 +12,29 @@ function (angular) { name: $scope.dashboard.title }; - $scope.createSnapshot = function() { + $scope.createSnapshot = function(makePublic) { $scope.dashboard.snapshot = true; $scope.loading = true; $rootScope.$broadcast('refresh'); $timeout(function() { var dash = angular.copy($scope.dashboard); - backendSrv.post('/api/snapshots/', {dashboard: dash}).then(function(results) { + dash.title = $scope.snapshot.name; + + var apiUrl = '/api/snapshots'; + + if (makePublic) { + apiUrl = 'http://snapshots.raintank.io/api/snapshots'; + } + + backendSrv.post(apiUrl, {dashboard: dash}).then(function(results) { $scope.loading = false; var baseUrl = $location.absUrl().replace($location.url(), ""); + if (makePublic) { + baseUrl = 'http://snapshots.raintank.io'; + } + $scope.snapshotUrl = baseUrl + '/dashboard/snapshots/' + results.key; }, function() { From 7614ddb318b65133c3ddf5d77219a5601932dc8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 Mar 2015 13:58:30 -0400 Subject: [PATCH 14/64] Updated design for snapshot sharing dialog, #1596 --- pkg/api/api.go | 1 + src/app/features/dashboard/dashboardSrv.js | 10 ++ .../dashboard/partials/shareDashboard.html | 93 +++++++++++-------- .../features/dashboard/shareSnapshotCtrl.js | 5 + src/css/less/gfbox.less | 18 ++++ 5 files changed, 89 insertions(+), 38 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 4fc76a6e8ba..4683e95fa40 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -43,6 +43,7 @@ func Register(r *macaron.Macaron) { // dashboard snapshots r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot) + r.Get("/dashboard/snapshots/*", Index) r.Get("/api/snapshots/:key", GetDashboardSnapshot) // authed api diff --git a/src/app/features/dashboard/dashboardSrv.js b/src/app/features/dashboard/dashboardSrv.js index 90ec885ed41..77b19b3edd2 100644 --- a/src/app/features/dashboard/dashboardSrv.js +++ b/src/app/features/dashboard/dashboardSrv.js @@ -67,6 +67,16 @@ function (angular, $, kbn, _, moment) { return max + 1; }; + p.forEachPanel = function(callback) { + var i, j, row; + for (i = 0; i < this.rows.length; i++) { + row = this.rows[i]; + for (j = 0; j < row.panels.length; j++) { + callback(row.panels[j], row); + } + } + }; + p.rowSpan = function(row) { return _.reduce(row.panels, function(p,v) { return p + v.span; diff --git a/src/app/features/dashboard/partials/shareDashboard.html b/src/app/features/dashboard/partials/shareDashboard.html index 1afd4a20a84..bfe126b390f 100644 --- a/src/app/features/dashboard/partials/shareDashboard.html +++ b/src/app/features/dashboard/partials/shareDashboard.html @@ -1,7 +1,7 @@ -
    -
    Share dashboard and data with anyone
    -

    - +

    diff --git a/src/app/features/dashboard/shareSnapshotCtrl.js b/src/app/features/dashboard/shareSnapshotCtrl.js index acc0f38d946..6f4e21cd70d 100644 --- a/src/app/features/dashboard/shareSnapshotCtrl.js +++ b/src/app/features/dashboard/shareSnapshotCtrl.js @@ -21,6 +21,11 @@ function (angular) { var dash = angular.copy($scope.dashboard); dash.title = $scope.snapshot.name; + dash.forEachPanel(function(panel){ + panel.targets = []; + panel.links = []; + }); + var apiUrl = '/api/snapshots'; if (makePublic) { diff --git a/src/css/less/gfbox.less b/src/css/less/gfbox.less index 12b16974162..995ccb5b435 100644 --- a/src/css/less/gfbox.less +++ b/src/css/less/gfbox.less @@ -96,3 +96,21 @@ } } } + +.share-snapshot { + text-align: center; + + .share-snapshot-header { + .fa { + position: absolute; + font-size: 600%; + left: 41%; + color: @grafanaTargetFuncBackground; + z-index: -1; + } + + position: relative; + z-index: 1000; + line-height: 106px; + } +} From 4d13a5bffb6f9b2fadb26d187b7e65aa11421293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 Mar 2015 14:00:03 -0400 Subject: [PATCH 15/64] Fixed failing style check --- src/app/features/dashboard/sharePanelCtrl.js | 11 ----------- src/app/features/dashboard/shareSnapshotCtrl.js | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/app/features/dashboard/sharePanelCtrl.js b/src/app/features/dashboard/sharePanelCtrl.js index 88710660e3c..c7303ab1a68 100644 --- a/src/app/features/dashboard/sharePanelCtrl.js +++ b/src/app/features/dashboard/sharePanelCtrl.js @@ -81,17 +81,6 @@ function (angular, _, require, config) { $scope.imageUrl += '&height=500'; }; - $scope.snapshot = function() { - $scope.dashboard.snapshot = true; - $rootScope.$broadcast('refresh'); - - $timeout(function() { - $scope.exportDashboard(); - $scope.dashboard.snapshot = false; - $scope.appEvent('dashboard-snapshot-cleanup'); - }, 1000); - }; - $scope.init(); }); diff --git a/src/app/features/dashboard/shareSnapshotCtrl.js b/src/app/features/dashboard/shareSnapshotCtrl.js index 6f4e21cd70d..88247e3138b 100644 --- a/src/app/features/dashboard/shareSnapshotCtrl.js +++ b/src/app/features/dashboard/shareSnapshotCtrl.js @@ -21,7 +21,7 @@ function (angular) { var dash = angular.copy($scope.dashboard); dash.title = $scope.snapshot.name; - dash.forEachPanel(function(panel){ + dash.forEachPanel(function(panel) { panel.targets = []; panel.links = []; }); From 41820ccb0507cab11f333a265218ea9b626d1943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 Mar 2015 15:32:38 -0400 Subject: [PATCH 16/64] Dashboard Snapshot sharing: singlestat panel now works, #1623 --- src/app/features/panel/panelHelper.js | 8 +++++++- src/app/features/panel/panelSrv.js | 7 +++++++ src/app/panels/graph/module.js | 15 ++++++--------- src/app/panels/singlestat/module.js | 5 +++++ 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/app/features/panel/panelHelper.js b/src/app/features/panel/panelHelper.js index c2842fb225e..62982f69438 100644 --- a/src/app/features/panel/panelHelper.js +++ b/src/app/features/panel/panelHelper.js @@ -70,7 +70,13 @@ function (angular, _, kbn, $) { cacheTimeout: scope.panel.cacheTimeout }; - return datasource.query(metricsQuery); + return datasource.query(metricsQuery).then(function(results) { + if (scope.dashboard.snapshot) { + scope.panel.snapshotData = results; + } + + return results; + }); }; }); diff --git a/src/app/features/panel/panelSrv.js b/src/app/features/panel/panelSrv.js index 7ce3d69b376..8113194dfaf 100644 --- a/src/app/features/panel/panelSrv.js +++ b/src/app/features/panel/panelSrv.js @@ -93,6 +93,13 @@ function (angular, _, config) { $scope.get_data = function() { if ($scope.otherPanelInFullscreenMode()) { return; } + if ($scope.panel.snapshotData) { + if ($scope.loadSnapshot) { + $scope.loadSnapshot($scope.panel.snapshotData); + } + return; + } + delete $scope.panelMeta.error; $scope.panelMeta.loading = true; diff --git a/src/app/panels/graph/module.js b/src/app/panels/graph/module.js index 966e1f9aa23..c7612c7cc9f 100644 --- a/src/app/panels/graph/module.js +++ b/src/app/panels/graph/module.js @@ -130,12 +130,6 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { $scope.refreshData = function(datasource) { panelHelper.updateTimeRange($scope); - if ($scope.panel.snapshotData) { - $scope.annotationsPromise = $q.when([]); - $scope.dataHandler($scope.panel.snapshotData); - return; - } - $scope.annotationsPromise = annotationsSrv.getAnnotations($scope.rangeUnparsed, $scope.dashboard); return panelHelper.issueMetricQuery($scope, datasource) @@ -146,10 +140,13 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { }); }; + $scope.loadSnapshot = function(snapshotData) { + panelHelper.updateTimeRange($scope); + $scope.annotationsPromise = $q.when([]); + $scope.dataHandler(snapshotData); + }; + $scope.dataHandler = function(results) { - if ($scope.dashboard.snapshot) { - $scope.panel.snapshotData = results; - } // png renderer returns just a url if (_.isString(results)) { $scope.render(results); diff --git a/src/app/panels/singlestat/module.js b/src/app/panels/singlestat/module.js index 81167a83a7d..8f33ce57cae 100644 --- a/src/app/panels/singlestat/module.js +++ b/src/app/panels/singlestat/module.js @@ -87,6 +87,11 @@ function (angular, app, _, TimeSeries, kbn, PanelMeta) { }); }; + $scope.loadSnapshot = function(snapshotData) { + panelHelper.updateTimeRange($scope); + $scope.dataHandler(snapshotData); + }; + $scope.dataHandler = function(results) { $scope.series = _.map(results.data, $scope.seriesHandler); $scope.render(); From 6f2a8e27b8a521439630fb13b703510a30856f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 Mar 2015 15:36:18 -0400 Subject: [PATCH 17/64] Dashboard Snapshot: added dashboard snapshot to changelog, #1623 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15e1527ad8c..690c94a0fbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 2.0.0 (unreleased) **New features** +- [Issue #1623](https://github.com/grafana/grafana/issues/1623). Share Dashboard: Dashboard snapshot sharing (dash and data snapshot), save to local or save to public snapshot dashboard snapshots.raintank.io site - [Issue #1622](https://github.com/grafana/grafana/issues/1622). Share Panel: The share modal now has an embed option, gives you an iframe that you can use to embedd a single graph on another web site - [Issue #718](https://github.com/grafana/grafana/issues/718). Dashboard: When saving a dashboard and another user has made changes inbetween the user is promted with a warning if he really wants to overwrite the other's changes - [Issue #1331](https://github.com/grafana/grafana/issues/1331). Graph & Singlestat: New axis/unit format selector and more units (kbytes, Joule, Watt, eV), and new design for graph axis & grid tab and single stat options tab views From 98c0209976a16bf7e604837718e285ddd62e21c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 Mar 2015 17:34:41 -0400 Subject: [PATCH 18/64] Dashboard snapshot: cleanup snapshot data after snapshot, #1623 --- src/app/features/dashboard/shareSnapshotCtrl.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/features/dashboard/shareSnapshotCtrl.js b/src/app/features/dashboard/shareSnapshotCtrl.js index 88247e3138b..6a787cee752 100644 --- a/src/app/features/dashboard/shareSnapshotCtrl.js +++ b/src/app/features/dashboard/shareSnapshotCtrl.js @@ -26,6 +26,12 @@ function (angular) { panel.links = []; }); + // cleanup snapshotData + $scope.dashboard.snapshot = false; + $scope.dashboard.forEachPanel(function(panel) { + delete panel.snapshotData; + }); + var apiUrl = '/api/snapshots'; if (makePublic) { @@ -46,8 +52,7 @@ function (angular) { $scope.loading = false; }); - $scope.dashboard.snapshot = false; - $scope.appEvent('dashboard-snapshot-cleanup'); + }, 2000); }; From 5f0e7cd52a3e78f02190cc769f91e52f27748445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 Mar 2015 18:28:59 -0400 Subject: [PATCH 19/64] Added custom cache control headers for static content --- conf/defaults.ini | 7 +- pkg/api/dashboard_snapshot.go | 1 + pkg/api/static/static.go | 218 ++++++++++++++++++ pkg/cmd/web.go | 20 +- .../features/dashboard/shareSnapshotCtrl.js | 58 ++--- tasks/options/requirejs.js | 1 + 6 files changed, 267 insertions(+), 38 deletions(-) create mode 100644 pkg/api/static/static.go diff --git a/conf/defaults.ini b/conf/defaults.ini index d35f71fa4ce..2e4ea89b3f2 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1,10 +1,9 @@ app_name = Grafana app_mode = production -# Once every 1 hour Grafana will report anonymous data to -# stats.grafana.org (https). No ip addresses are being tracked. -# only simple counters to track running instances, dashboard -# count and errors. It is very helpful to us. +# Report anonymous usage counters to stats.grafana.org (https). +# No ip addresses are being tracked, only simple counters to track +# running instances, dashboard count and errors. It is very helpful to us. # Change this option to false to disable reporting. reporting-enabled = true diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 979a3aa86f2..e4841074901 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -35,5 +35,6 @@ func GetDashboardSnapshot(c *middleware.Context) { Meta: dtos.DashboardMeta{IsSnapshot: true}, } + c.Resp.Header().Set("Cache-Control", "public max-age: 31536000") c.JSON(200, dto) } diff --git a/pkg/api/static/static.go b/pkg/api/static/static.go new file mode 100644 index 00000000000..43ba6a32b20 --- /dev/null +++ b/pkg/api/static/static.go @@ -0,0 +1,218 @@ +// Copyright 2013 Martini Authors +// Copyright 2014 Unknwon +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package httpstatic + +import ( + "log" + "net/http" + "os" + "path" + "path/filepath" + "strings" + "sync" + + "github.com/Unknwon/macaron" +) + +var Root string + +func init() { + var err error + Root, err = os.Getwd() + if err != nil { + panic("error getting work directory: " + err.Error()) + } +} + +// StaticOptions is a struct for specifying configuration options for the macaron.Static middleware. +type StaticOptions struct { + // Prefix is the optional prefix used to serve the static directory content + Prefix string + // SkipLogging will disable [Static] log messages when a static file is served. + SkipLogging bool + // IndexFile defines which file to serve as index if it exists. + IndexFile string + // Expires defines which user-defined function to use for producing a HTTP Expires Header + // https://developers.google.com/speed/docs/insights/LeverageBrowserCaching + AddHeaders func(ctx *macaron.Context) + // FileSystem is the interface for supporting any implmentation of file system. + FileSystem http.FileSystem +} + +// FIXME: to be deleted. +type staticMap struct { + lock sync.RWMutex + data map[string]*http.Dir +} + +func (sm *staticMap) Set(dir *http.Dir) { + sm.lock.Lock() + defer sm.lock.Unlock() + + sm.data[string(*dir)] = dir +} + +func (sm *staticMap) Get(name string) *http.Dir { + sm.lock.RLock() + defer sm.lock.RUnlock() + + return sm.data[name] +} + +func (sm *staticMap) Delete(name string) { + sm.lock.Lock() + defer sm.lock.Unlock() + + delete(sm.data, name) +} + +var statics = staticMap{sync.RWMutex{}, map[string]*http.Dir{}} + +// staticFileSystem implements http.FileSystem interface. +type staticFileSystem struct { + dir *http.Dir +} + +func newStaticFileSystem(directory string) staticFileSystem { + if !filepath.IsAbs(directory) { + directory = filepath.Join(Root, directory) + } + dir := http.Dir(directory) + statics.Set(&dir) + return staticFileSystem{&dir} +} + +func (fs staticFileSystem) Open(name string) (http.File, error) { + return fs.dir.Open(name) +} + +func prepareStaticOption(dir string, opt StaticOptions) StaticOptions { + // Defaults + if len(opt.IndexFile) == 0 { + opt.IndexFile = "index.html" + } + // Normalize the prefix if provided + if opt.Prefix != "" { + // Ensure we have a leading '/' + if opt.Prefix[0] != '/' { + opt.Prefix = "/" + opt.Prefix + } + // Remove any trailing '/' + opt.Prefix = strings.TrimRight(opt.Prefix, "/") + } + if opt.FileSystem == nil { + opt.FileSystem = newStaticFileSystem(dir) + } + return opt +} + +func prepareStaticOptions(dir string, options []StaticOptions) StaticOptions { + var opt StaticOptions + if len(options) > 0 { + opt = options[0] + } + return prepareStaticOption(dir, opt) +} + +func staticHandler(ctx *macaron.Context, log *log.Logger, opt StaticOptions) bool { + if ctx.Req.Method != "GET" && ctx.Req.Method != "HEAD" { + return false + } + + file := ctx.Req.URL.Path + // if we have a prefix, filter requests by stripping the prefix + if opt.Prefix != "" { + if !strings.HasPrefix(file, opt.Prefix) { + return false + } + file = file[len(opt.Prefix):] + if file != "" && file[0] != '/' { + return false + } + } + + f, err := opt.FileSystem.Open(file) + if err != nil { + return false + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return true // File exists but fail to open. + } + + // Try to serve index file + if fi.IsDir() { + // Redirect if missing trailing slash. + if !strings.HasSuffix(ctx.Req.URL.Path, "/") { + http.Redirect(ctx.Resp, ctx.Req.Request, ctx.Req.URL.Path+"/", http.StatusFound) + return true + } + + file = path.Join(file, opt.IndexFile) + f, err = opt.FileSystem.Open(file) + if err != nil { + return false // Discard error. + } + defer f.Close() + + fi, err = f.Stat() + if err != nil || fi.IsDir() { + return true + } + } + + if !opt.SkipLogging { + log.Println("[Static] Serving " + file) + } + + // Add an Expires header to the static content + if opt.AddHeaders != nil { + opt.AddHeaders(ctx) + } + + http.ServeContent(ctx.Resp, ctx.Req.Request, file, fi.ModTime(), f) + return true +} + +// Static returns a middleware handler that serves static files in the given directory. +func Static(directory string, staticOpt ...StaticOptions) macaron.Handler { + opt := prepareStaticOptions(directory, staticOpt) + + return func(ctx *macaron.Context, log *log.Logger) { + staticHandler(ctx, log, opt) + } +} + +// Statics registers multiple static middleware handlers all at once. +func Statics(opt StaticOptions, dirs ...string) macaron.Handler { + if len(dirs) == 0 { + panic("no static directory is given") + } + opts := make([]StaticOptions, len(dirs)) + for i := range dirs { + opts[i] = prepareStaticOption(dirs[i], opt) + } + + return func(ctx *macaron.Context, log *log.Logger) { + for i := range opts { + if staticHandler(ctx, log, opts[i]) { + return + } + } + } +} diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index e5516fb52d9..a8482185e78 100644 --- a/pkg/cmd/web.go +++ b/pkg/cmd/web.go @@ -11,7 +11,6 @@ import ( "path" "path/filepath" "strconv" - "time" "github.com/Unknwon/macaron" "github.com/codegangsta/cli" @@ -20,6 +19,7 @@ import ( _ "github.com/macaron-contrib/session/postgres" "github.com/grafana/grafana/pkg/api" + "github.com/grafana/grafana/pkg/api/static" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" @@ -65,14 +65,22 @@ func newMacaron() *macaron.Macaron { } func mapStatic(m *macaron.Macaron, dir string, prefix string) { - m.Use(macaron.Static( + headers := func(c *macaron.Context) { + c.Resp.Header().Set("Cache-Control", "public max-age: 3600") + } + + if setting.Env == setting.DEV { + headers = func(c *macaron.Context) { + c.Resp.Header().Set("Cache-Control", "max-age: 0") + } + } + + m.Use(httpstatic.Static( path.Join(setting.StaticRootPath, dir), - macaron.StaticOptions{ + httpstatic.StaticOptions{ SkipLogging: true, Prefix: prefix, - Expires: func() string { - return time.Now().UTC().Format(http.TimeFormat) - }, + AddHeaders: headers, }, )) } diff --git a/src/app/features/dashboard/shareSnapshotCtrl.js b/src/app/features/dashboard/shareSnapshotCtrl.js index 6a787cee752..fc7973a676c 100644 --- a/src/app/features/dashboard/shareSnapshotCtrl.js +++ b/src/app/features/dashboard/shareSnapshotCtrl.js @@ -18,42 +18,44 @@ function (angular) { $rootScope.$broadcast('refresh'); $timeout(function() { - var dash = angular.copy($scope.dashboard); - dash.title = $scope.snapshot.name; + $scope.saveSnapshot(makePublic); + }, 2000); + }; - dash.forEachPanel(function(panel) { - panel.targets = []; - panel.links = []; - }); + $scope.saveSnapshot = function(makePublic) { + var dash = angular.copy($scope.dashboard); + dash.title = $scope.snapshot.name; - // cleanup snapshotData - $scope.dashboard.snapshot = false; - $scope.dashboard.forEachPanel(function(panel) { - delete panel.snapshotData; - }); + dash.forEachPanel(function(panel) { + panel.targets = []; + panel.links = []; + }); - var apiUrl = '/api/snapshots'; + // cleanup snapshotData + $scope.dashboard.snapshot = false; + $scope.dashboard.forEachPanel(function(panel) { + delete panel.snapshotData; + }); + var apiUrl = '/api/snapshots'; + + if (makePublic) { + apiUrl = 'http://snapshots.raintank.io/api/snapshots'; + } + + backendSrv.post(apiUrl, {dashboard: dash}).then(function(results) { + $scope.loading = false; + + var baseUrl = $location.absUrl().replace($location.url(), ""); if (makePublic) { - apiUrl = 'http://snapshots.raintank.io/api/snapshots'; + baseUrl = 'http://snapshots.raintank.io'; } - backendSrv.post(apiUrl, {dashboard: dash}).then(function(results) { - $scope.loading = false; + $scope.snapshotUrl = baseUrl + '/dashboard/snapshots/' + results.key; - var baseUrl = $location.absUrl().replace($location.url(), ""); - if (makePublic) { - baseUrl = 'http://snapshots.raintank.io'; - } - - $scope.snapshotUrl = baseUrl + '/dashboard/snapshots/' + results.key; - - }, function() { - $scope.loading = false; - }); - - - }, 2000); + }, function() { + $scope.loading = false; + }); }; }); diff --git a/tasks/options/requirejs.js b/tasks/options/requirejs.js index 947553f1266..d9edf76ada4 100644 --- a/tasks/options/requirejs.js +++ b/tasks/options/requirejs.js @@ -61,6 +61,7 @@ module.exports = function(config,grunt) { 'controllers/all', 'routes/all', 'components/partials', + 'plugins/datasource/grafana/datasource', ] } ]; From 3e9adeefbcf81ba1633801582fe8e553dfeeb73d Mon Sep 17 00:00:00 2001 From: Matt Robenolt Date: Mon, 23 Mar 2015 21:58:29 -0700 Subject: [PATCH 20/64] Fix format of Cache-Control header --- pkg/api/dashboard_snapshot.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index e4841074901..aa20c4b8a7f 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -35,6 +35,6 @@ func GetDashboardSnapshot(c *middleware.Context) { Meta: dtos.DashboardMeta{IsSnapshot: true}, } - c.Resp.Header().Set("Cache-Control", "public max-age: 31536000") + c.Resp.Header().Set("Cache-Control", "public, max-age=31536000") c.JSON(200, dto) } From 527e802b05d808531381ce3020e34f19c8a3aeb1 Mon Sep 17 00:00:00 2001 From: Stefan Wehner Date: Tue, 24 Mar 2015 11:37:26 +0100 Subject: [PATCH 21/64] Limit ElasticSearch return to title and tags --- src/app/features/elasticsearch/datasource.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/features/elasticsearch/datasource.js b/src/app/features/elasticsearch/datasource.js index 3c82d98f8d2..8fb9d8c12d5 100644 --- a/src/app/features/elasticsearch/datasource.js +++ b/src/app/features/elasticsearch/datasource.js @@ -270,7 +270,8 @@ function (angular, _, config, kbn, moment) { query: { query_string: { query: queryString } }, facets: { tags: { terms: { field: "tags", order: "term", size: 50 } } }, size: this.searchMaxResults, - sort: ["_uid"] + sort: ["_uid"], + fields: ["title", "tags"] }; return this._post('/dashboard/_search', query) @@ -286,8 +287,8 @@ function (angular, _, config, kbn, moment) { var hit = resultsHits[i]; displayHits.dashboards.push({ id: hit._id, - title: hit._source.title, - tags: hit._source.tags + title: hit.fields.title, + tags: hit.fields.tags }); } From c27db7a3471c0bb61abb5e6cf56025ce1339a4ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 24 Mar 2015 15:45:31 +0100 Subject: [PATCH 22/64] Small updates to share dashboard snapshot feature --- pkg/api/dashboard_snapshot.go | 2 +- .../dashboard/partials/shareDashboard.html | 47 +++++++++---------- .../features/dashboard/shareSnapshotCtrl.js | 7 ++- src/css/less/gfbox.less | 16 ------- src/css/less/grafana.less | 27 +++++++++++ 5 files changed, 55 insertions(+), 44 deletions(-) diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index aa20c4b8a7f..8bb6c6e6df3 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -9,7 +9,7 @@ import ( ) func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) { - cmd.Key = util.GetRandomString(20) + cmd.Key = util.GetRandomString(32) if err := bus.Dispatch(&cmd); err != nil { c.JsonApiErr(500, "Failed to create snaphost", err) diff --git a/src/app/features/dashboard/partials/shareDashboard.html b/src/app/features/dashboard/partials/shareDashboard.html index bfe126b390f..320042b64bf 100644 --- a/src/app/features/dashboard/partials/shareDashboard.html +++ b/src/app/features/dashboard/partials/shareDashboard.html @@ -50,36 +50,33 @@
    -
    - -
    -
    -
      -
    • - Snapshot name -
    • -
    • - -
    • -
    -
    -
    +
    +
    +
      +
    • + Snapshot name +
    • +
    • + +
    • +
    +
    +
    -
    - +
    +
    + +
    +
    +
    diff --git a/src/app/features/dashboard/shareSnapshotCtrl.js b/src/app/features/dashboard/shareSnapshotCtrl.js index fc7973a676c..c9a71f4230b 100644 --- a/src/app/features/dashboard/shareSnapshotCtrl.js +++ b/src/app/features/dashboard/shareSnapshotCtrl.js @@ -6,7 +6,7 @@ function (angular) { var module = angular.module('grafana.controllers'); - module.controller('ShareSnapshotCtrl', function($scope, $rootScope, $location, backendSrv, $timeout) { + module.controller('ShareSnapshotCtrl', function($scope, $rootScope, $location, backendSrv, $timeout, timeSrv) { $scope.snapshot = { name: $scope.dashboard.title @@ -24,8 +24,11 @@ function (angular) { $scope.saveSnapshot = function(makePublic) { var dash = angular.copy($scope.dashboard); + // change title dash.title = $scope.snapshot.name; - + // make relative times absolute + dash.time = timeSrv.timeRange(); + // remove panel queries & links dash.forEachPanel(function(panel) { panel.targets = []; panel.links = []; diff --git a/src/css/less/gfbox.less b/src/css/less/gfbox.less index 995ccb5b435..50401e21c50 100644 --- a/src/css/less/gfbox.less +++ b/src/css/less/gfbox.less @@ -97,20 +97,4 @@ } } -.share-snapshot { - text-align: center; - .share-snapshot-header { - .fa { - position: absolute; - font-size: 600%; - left: 41%; - color: @grafanaTargetFuncBackground; - z-index: -1; - } - - position: relative; - z-index: 1000; - line-height: 106px; - } -} diff --git a/src/css/less/grafana.less b/src/css/less/grafana.less index 579415f17f8..6b6e17ce834 100644 --- a/src/css/less/grafana.less +++ b/src/css/less/grafana.less @@ -294,3 +294,30 @@ } } } + +.share-snapshot { + text-align: center; + + .share-snapshot-header { + .fa { + position: absolute; + font-size: 600%; + left: 42%; + color: @grafanaTargetFuncBackground; + z-index: -1; + } + + position: relative; + z-index: 1000; + line-height: 106px; + margin: 45px 0 22px 0; + } + + .share-snapshot-link { + max-width: 716px; + white-space: nowrap; + overflow: hidden; + display: block; + text-overflow: ellipsis; + } +} From ddd3df26b103beb5e3b24fc845e3b19b5ef01bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 24 Mar 2015 15:52:22 +0100 Subject: [PATCH 23/64] Fixed docs spelling issue, #1634 --- docs/sources/installation/performance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/installation/performance.md b/docs/sources/installation/performance.md index 535cf72a228..ce41e7a0548 100644 --- a/docs/sources/installation/performance.md +++ b/docs/sources/installation/performance.md @@ -11,6 +11,6 @@ page_keywords: grafana, performance, documentation Graphite 0.9.13 adds a much needed feature to the json rendering API that is very important for Grafana. If you are experiance slow load & rendering times for large time ranges then it is most likely caused by running Graphite 0.9.12 or lower. The latest version of Graphite adds a maxDataPoints parameter to the json render API, without this feature Graphite can return hundreds of thousands of data points per graph, which -can hang your browser. Be sue to upgrade to [0.9.13](http://graphite.readthedocs.org/en/latest/releases/0_9_13.html). +can hang your browser. Be sure to upgrade to [0.9.13](http://graphite.readthedocs.org/en/latest/releases/0_9_13.html). From a5c3855233b20dcbff641541a2a78de4a54e6237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 24 Mar 2015 16:49:12 +0100 Subject: [PATCH 24/64] Added dashboard snapshot metrics --- pkg/api/dashboard_snapshot.go | 6 ++++++ pkg/metrics/metrics.go | 3 +++ 2 files changed, 9 insertions(+) diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 8bb6c6e6df3..635e1d14711 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -3,6 +3,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" @@ -16,6 +17,8 @@ func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapsho return } + metrics.M_Api_Dashboard_Snapshot_Create.Inc(1) + c.JSON(200, util.DynMap{"key": cmd.Key}) } @@ -35,6 +38,9 @@ func GetDashboardSnapshot(c *middleware.Context) { Meta: dtos.DashboardMeta{IsSnapshot: true}, } + metrics.M_Api_Dashboard_Snapshot_Get.Inc(1) + c.Resp.Header().Set("Cache-Control", "public, max-age=31536000") + c.JSON(200, dto) } diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 45b964fb56e..71a5aeaacf5 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -21,5 +21,8 @@ var ( M_Api_Login_OAuth = NewComboCounterRef("api.login.oauth") M_Api_Org_Create = NewComboCounterRef("api.org.create") + M_Api_Dashboard_Snapshot_Create = NewComboCounterRef("api.dashboard_snapshot.create") + M_Api_Dashboard_Snapshot_Get = NewComboCounterRef("api.dashboard_snapshot.get") + M_Models_Dashboard_Insert = NewComboCounterRef("models.dashboard.insert") ) From 7919d79347792b7d93de448844a0ce3fbb289de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 24 Mar 2015 17:16:13 +0100 Subject: [PATCH 25/64] Another cache header fix --- pkg/cmd/web.go | 4 ++-- src/app/features/dashboard/rowCtrl.js | 8 ++++++++ src/app/partials/dashboard.html | 8 ++++++++ src/css/less/panel.less | 12 ++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index a8482185e78..72f19f6675f 100644 --- a/pkg/cmd/web.go +++ b/pkg/cmd/web.go @@ -66,12 +66,12 @@ func newMacaron() *macaron.Macaron { func mapStatic(m *macaron.Macaron, dir string, prefix string) { headers := func(c *macaron.Context) { - c.Resp.Header().Set("Cache-Control", "public max-age: 3600") + c.Resp.Header().Set("Cache-Control", "public, max-age: 3600") } if setting.Env == setting.DEV { headers = func(c *macaron.Context) { - c.Resp.Header().Set("Cache-Control", "max-age: 0") + c.Resp.Header().Set("Cache-Control", "max-age: 0, must-revalidate") } } diff --git a/src/app/features/dashboard/rowCtrl.js b/src/app/features/dashboard/rowCtrl.js index 409fee2fd37..527a29a2ad5 100644 --- a/src/app/features/dashboard/rowCtrl.js +++ b/src/app/features/dashboard/rowCtrl.js @@ -168,4 +168,12 @@ function (angular, app, _, config) { }; }); + module.directive('panelGhostPanel', function() { + return function(scope, element) { + var dropZoneSpan = 12 - scope.dashboard.rowSpan(scope.row); + element.find('.panel-container').css('height', scope.row.height); + element[0].style.width = ((dropZoneSpan / 1.2) * 10) + '%'; + }; + }); + }); diff --git a/src/app/partials/dashboard.html b/src/app/partials/dashboard.html index 6d7d0634fcd..29c4d581df3 100644 --- a/src/app/partials/dashboard.html +++ b/src/app/partials/dashboard.html @@ -86,6 +86,14 @@
    +
    +
    +
    +

    Add panel

    +
    +
    +
    +
    diff --git a/src/css/less/panel.less b/src/css/less/panel.less index 6a8c2355b5b..e3f1fd2b598 100644 --- a/src/css/less/panel.less +++ b/src/css/less/panel.less @@ -169,6 +169,18 @@ } } +.ghost-panel { + &:hover { + .panel-container { + visibility: visible; + } + } + .panel-container { + visibility: hidden; + border: 1px solid @grayDark; + } +} + .panel-time-info { font-weight: bold; float: right; From cc71b1f07d6ad3a4e069f0f4fa22a4bfbb7187af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 24 Mar 2015 19:42:39 +0100 Subject: [PATCH 26/64] Ghost panel test --- src/app/features/dashboard/rowCtrl.js | 4 ++-- src/app/features/panel/panelMenu.js | 6 +++--- src/app/features/panel/panelSrv.js | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/app/features/dashboard/rowCtrl.js b/src/app/features/dashboard/rowCtrl.js index 527a29a2ad5..2e18291719d 100644 --- a/src/app/features/dashboard/rowCtrl.js +++ b/src/app/features/dashboard/rowCtrl.js @@ -81,13 +81,13 @@ function (angular, app, _, config) { $scope.$broadcast('render'); }; - $scope.remove_panel_from_row = function(row, panel) { + $scope.removePanel = function(panel) { $scope.appEvent('confirm-modal', { title: 'Are you sure you want to remove this panel?', icon: 'fa-trash', yesText: 'Delete', onConfirm: function() { - row.panels = _.without(row.panels, panel); + $scope.row.panels = _.without($scope.row.panels, panel); } }); }; diff --git a/src/app/features/panel/panelMenu.js b/src/app/features/panel/panelMenu.js index a529dd87b5c..96fcc521f49 100644 --- a/src/app/features/panel/panelMenu.js +++ b/src/app/features/panel/panelMenu.js @@ -20,9 +20,9 @@ function (angular, $, _) { var template = '
    '; template += '
    '; template += '
    '; - template += ''; - template += ''; - template += ''; + template += ''; + template += ''; + template += ''; template += '
    '; template += '
    '; diff --git a/src/app/features/panel/panelSrv.js b/src/app/features/panel/panelSrv.js index 8113194dfaf..4c5e34cdbfa 100644 --- a/src/app/features/panel/panelSrv.js +++ b/src/app/features/panel/panelSrv.js @@ -41,6 +41,7 @@ function (angular, _, config) { $scope.updateColumnSpan = function(span) { $scope.panel.span = Math.min(Math.max($scope.panel.span + span, 1), 12); + $scope.row.updatePanelSpan() $timeout(function() { $scope.$broadcast('render'); From f9cf673f81f8d5c0520137ebd62e2ad3b526fd6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 24 Mar 2015 19:49:51 +0100 Subject: [PATCH 27/64] removed accidental code, should have been part of ghost-panel branch commit --- src/app/partials/dashboard.html | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/app/partials/dashboard.html b/src/app/partials/dashboard.html index 29c4d581df3..6d7d0634fcd 100644 --- a/src/app/partials/dashboard.html +++ b/src/app/partials/dashboard.html @@ -86,14 +86,6 @@
    -
    -
    -
    -

    Add panel

    -
    -
    -
    -
    From 789363b0ad7e8f3d9d7b0ee80d711b4952e2b161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 24 Mar 2015 21:10:44 +0100 Subject: [PATCH 28/64] Added ghost panel that shows up empty rows, this panel will show add panel buttons to more quickly/easier get to add a panel, #1635 --- src/app/features/dashboard/rowCtrl.js | 33 ++++++++++++++++++--------- src/app/features/panel/panelMenu.js | 4 ++-- src/app/features/panel/panelSrv.js | 3 +-- src/app/partials/dashboard.html | 22 +++++++++++------- src/app/partials/roweditor.html | 28 ++--------------------- src/css/less/panel.less | 24 ++++++++++++------- 6 files changed, 57 insertions(+), 57 deletions(-) diff --git a/src/app/features/dashboard/rowCtrl.js b/src/app/features/dashboard/rowCtrl.js index 2e18291719d..e2ab9a0c156 100644 --- a/src/app/features/dashboard/rowCtrl.js +++ b/src/app/features/dashboard/rowCtrl.js @@ -38,11 +38,6 @@ function (angular, app, _, config) { } }; - // This can be overridden by individual panels - $scope.close_edit = function() { - $scope.$broadcast('render'); - }; - $scope.add_panel = function(panel) { $scope.dashboard.add_panel(panel, $scope.row); }; @@ -92,6 +87,10 @@ function (angular, app, _, config) { }); }; + $scope.updatePanelSpan = function(panel, span) { + panel.span = Math.min(Math.max(panel.span + span, 1), 12); + }; + $scope.replacePanel = function(newPanel, oldPanel) { var row = $scope.row; var index = _.indexOf(row.panels, oldPanel); @@ -144,9 +143,11 @@ function (angular, app, _, config) { module.directive('panelWidth', function() { return function(scope, element) { - scope.$watch('panel.span', function() { + function updateWidth() { element[0].style.width = ((scope.panel.span / 1.2) * 10) + '%'; - }); + } + + scope.$watch('panel.span', updateWidth); }; }); @@ -168,11 +169,21 @@ function (angular, app, _, config) { }; }); - module.directive('panelGhostPanel', function() { + module.directive('panelGhost', function() { return function(scope, element) { - var dropZoneSpan = 12 - scope.dashboard.rowSpan(scope.row); - element.find('.panel-container').css('height', scope.row.height); - element[0].style.width = ((dropZoneSpan / 1.2) * 10) + '%'; + function updateWidth() { + var spanLeft = 12 - scope.dashboard.rowSpan(scope.row); + if (spanLeft > 1) { + element.show(); + element.find('.panel-container').css('height', scope.row.height); + element[0].style.width = ((spanLeft / 1.2) * 10) + '%'; + } else { + element.hide(); + } + } + + updateWidth(); + scope.$on('dashboard-panel-span-updated', updateWidth); }; }); diff --git a/src/app/features/panel/panelMenu.js b/src/app/features/panel/panelMenu.js index 96fcc521f49..1f7d6aba71c 100644 --- a/src/app/features/panel/panelMenu.js +++ b/src/app/features/panel/panelMenu.js @@ -20,8 +20,8 @@ function (angular, $, _) { var template = '
    '; template += '
    '; template += '
    '; - template += ''; - template += ''; + template += ''; + template += ''; template += ''; template += '
    '; template += '
    '; diff --git a/src/app/features/panel/panelSrv.js b/src/app/features/panel/panelSrv.js index 4c5e34cdbfa..0e983fc3d07 100644 --- a/src/app/features/panel/panelSrv.js +++ b/src/app/features/panel/panelSrv.js @@ -40,8 +40,7 @@ function (angular, _, config) { }; $scope.updateColumnSpan = function(span) { - $scope.panel.span = Math.min(Math.max($scope.panel.span + span, 1), 12); - $scope.row.updatePanelSpan() + $scope.updatePanelSpan($scope.panel, span); $timeout(function() { $scope.$broadcast('render'); diff --git a/src/app/partials/dashboard.html b/src/app/partials/dashboard.html index 6d7d0634fcd..edfe1bfcafb 100644 --- a/src/app/partials/dashboard.html +++ b/src/app/partials/dashboard.html @@ -86,14 +86,20 @@
    -
    -
    -
    - Drop here -
    -
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + Drop here +
    +
    diff --git a/src/app/partials/roweditor.html b/src/app/partials/roweditor.html index 4c77a0bf814..177a8fd4ce5 100644 --- a/src/app/partials/roweditor.html +++ b/src/app/partials/roweditor.html @@ -5,7 +5,7 @@
    -
    +
    @@ -26,29 +26,5 @@
    -
    -
    - - - - - - - - - - - - - - - - - -
    TitleTypeSpan
    {{panel.title}}{{panel.type}} - - - -
    -
    +
    diff --git a/src/css/less/panel.less b/src/css/less/panel.less index e3f1fd2b598..20164acfb9e 100644 --- a/src/css/less/panel.less +++ b/src/css/less/panel.less @@ -169,15 +169,23 @@ } } -.ghost-panel { - &:hover { - .panel-container { - visibility: visible; - } - } +.panel-ghost{ + width: 100%; .panel-container { - visibility: hidden; - border: 1px solid @grayDark; + border: none; + background: transparent; + } + .panel-ghost-list { + margin: 10px 0 10px 20px; + } + + button { + text-align: left; + min-width: 135px; + .fa { + position: relative; + left: -5px; + } } } From 5286f0856d0f7c7765c709d0559d9ef168fbe8b1 Mon Sep 17 00:00:00 2001 From: Matt Robenolt Date: Tue, 24 Mar 2015 17:30:26 -0700 Subject: [PATCH 29/64] Fix more Cache-Control headers `max-age` is always with an `=`, not a `:`. --- pkg/cmd/web.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index 72f19f6675f..1fc6e8a999c 100644 --- a/pkg/cmd/web.go +++ b/pkg/cmd/web.go @@ -66,12 +66,12 @@ func newMacaron() *macaron.Macaron { func mapStatic(m *macaron.Macaron, dir string, prefix string) { headers := func(c *macaron.Context) { - c.Resp.Header().Set("Cache-Control", "public, max-age: 3600") + c.Resp.Header().Set("Cache-Control", "public, max-age=3600") } if setting.Env == setting.DEV { headers = func(c *macaron.Context) { - c.Resp.Header().Set("Cache-Control", "max-age: 0, must-revalidate") + c.Resp.Header().Set("Cache-Control", "max-age=0, must-revalidate, no-cache") } } From 9268ecf3e9ad0a94ee8e6d0dc6564dfc793d88ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 25 Mar 2015 09:04:38 +0100 Subject: [PATCH 30/64] Some refinements to dashboard snapshots --- pkg/api/dashboard_snapshot.go | 37 ++++++++++++++++++- pkg/metrics/metrics.go | 5 ++- pkg/metrics/report_usage.go | 19 +++++----- pkg/models/dashboard_snapshot.go | 1 + .../features/dashboard/shareSnapshotCtrl.js | 21 ++++------- 5 files changed, 56 insertions(+), 27 deletions(-) diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 635e1d14711..c4035e921d5 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -1,17 +1,27 @@ package api import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "time" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) { - cmd.Key = util.GetRandomString(32) + if cmd.External { + createExternalSnapshot(c, cmd) + } + cmd.Key = util.GetRandomString(32) if err := bus.Dispatch(&cmd); err != nil { c.JsonApiErr(500, "Failed to create snaphost", err) return @@ -19,7 +29,30 @@ func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapsho metrics.M_Api_Dashboard_Snapshot_Create.Inc(1) - c.JSON(200, util.DynMap{"key": cmd.Key}) + c.JSON(200, util.DynMap{"key": cmd.Key, "url": setting.ToAbsUrl("/dashboard/snapshots")}) +} + +func createExternalSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) { + metrics.M_Api_Dashboard_Snapshot_External.Inc(1) + + json, _ := json.Marshal(cmd) + jsonData := bytes.NewBuffer(json) + + client := http.Client{Timeout: time.Duration(5 * time.Second)} + resp, err := client.Post("http://snapshots-origin.raintank.io/api/snapshots", "application/json", jsonData) + + if err != nil { + c.JsonApiErr(500, "Failed to publish external snapshot", err) + return + } + + c.Header().Set("Content-Type", resp.Header.Get("Content-Type")) + c.WriteHeader(resp.StatusCode) + + if resp.ContentLength > 0 { + bytes, _ := ioutil.ReadAll(resp.Body) + c.Write(bytes) + } } func GetDashboardSnapshot(c *middleware.Context) { diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 71a5aeaacf5..f6dab8c8043 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -21,8 +21,9 @@ var ( M_Api_Login_OAuth = NewComboCounterRef("api.login.oauth") M_Api_Org_Create = NewComboCounterRef("api.org.create") - M_Api_Dashboard_Snapshot_Create = NewComboCounterRef("api.dashboard_snapshot.create") - M_Api_Dashboard_Snapshot_Get = NewComboCounterRef("api.dashboard_snapshot.get") + M_Api_Dashboard_Snapshot_Create = NewComboCounterRef("api.dashboard_snapshot.create") + M_Api_Dashboard_Snapshot_External = NewComboCounterRef("api.dashboard_snapshot.external") + M_Api_Dashboard_Snapshot_Get = NewComboCounterRef("api.dashboard_snapshot.get") M_Models_Dashboard_Insert = NewComboCounterRef("models.dashboard.insert") ) diff --git a/pkg/metrics/report_usage.go b/pkg/metrics/report_usage.go index f952e56fab6..4a4355c5deb 100644 --- a/pkg/metrics/report_usage.go +++ b/pkg/metrics/report_usage.go @@ -7,7 +7,9 @@ import ( "strings" "time" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) @@ -34,11 +36,11 @@ func sendUsageStats() { "metrics": metrics, } - // statsQuery := m.GetSystemStatsQuery{} - // if err := bus.Dispatch(&statsQuery); err != nil { - // log.Error(3, "Failed to get system stats", err) - // return - // } + statsQuery := m.GetSystemStatsQuery{} + if err := bus.Dispatch(&statsQuery); err != nil { + log.Error(3, "Failed to get system stats", err) + return + } UsageStats.Each(func(name string, i interface{}) { switch metric := i.(type) { @@ -50,14 +52,13 @@ func sendUsageStats() { } }) - // metrics["stats.dashboards.count"] = statsQuery.Result.DashboardCount - // metrics["stats.users.count"] = statsQuery.Result.UserCount - // metrics["stats.orgs.count"] = statsQuery.Result.OrgCount + metrics["stats.dashboards.count"] = statsQuery.Result.DashboardCount + metrics["stats.users.count"] = statsQuery.Result.UserCount + metrics["stats.orgs.count"] = statsQuery.Result.OrgCount out, _ := json.Marshal(report) data := bytes.NewBuffer(out) client := http.Client{Timeout: time.Duration(5 * time.Second)} - go client.Post("https://stats.grafana.org/grafana-usage-report", "application/json", data) } diff --git a/pkg/models/dashboard_snapshot.go b/pkg/models/dashboard_snapshot.go index 61abca12673..8f96b27f6ae 100644 --- a/pkg/models/dashboard_snapshot.go +++ b/pkg/models/dashboard_snapshot.go @@ -20,6 +20,7 @@ type DashboardSnapshot struct { type CreateDashboardSnapshotCommand struct { Dashboard map[string]interface{} `json:"dashboard" binding:"Required"` + External bool Key string `json:"-"` diff --git a/src/app/features/dashboard/shareSnapshotCtrl.js b/src/app/features/dashboard/shareSnapshotCtrl.js index c9a71f4230b..a3f279b9c64 100644 --- a/src/app/features/dashboard/shareSnapshotCtrl.js +++ b/src/app/features/dashboard/shareSnapshotCtrl.js @@ -22,7 +22,7 @@ function (angular) { }, 2000); }; - $scope.saveSnapshot = function(makePublic) { + $scope.saveSnapshot = function(external) { var dash = angular.copy($scope.dashboard); // change title dash.title = $scope.snapshot.name; @@ -40,22 +40,15 @@ function (angular) { delete panel.snapshotData; }); - var apiUrl = '/api/snapshots'; - - if (makePublic) { - apiUrl = 'http://snapshots.raintank.io/api/snapshots'; - } - - backendSrv.post(apiUrl, {dashboard: dash}).then(function(results) { + backendSrv.post('/api/snapshots', {dashboard: dash, external: external}).then(function(results) { $scope.loading = false; - var baseUrl = $location.absUrl().replace($location.url(), ""); - if (makePublic) { - baseUrl = 'http://snapshots.raintank.io'; + if (external) { + $scope.snapshotUrl = results.url; + } else { + var baseUrl = $location.absUrl().replace($location.url(), ""); + $scope.snapshotUrl = baseUrl + '/dashboard/snapshots/' + results.key; } - - $scope.snapshotUrl = baseUrl + '/dashboard/snapshots/' + results.key; - }, function() { $scope.loading = false; }); From da833cbc5853068c26a6a4a1475cf00576aafaf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 25 Mar 2015 11:07:12 +0100 Subject: [PATCH 31/64] Small progress on influxdb 0.9 query editor, #1525 --- src/app/directives/graphiteSegment.js | 4 +- .../plugins/datasource/graphite/queryCtrl.js | 19 +- .../plugins/datasource/influxdb/datasource.js | 74 ++--- .../influxdb/partials/query.editor.html | 284 ++++-------------- .../plugins/datasource/influxdb/queryCtrl.js | 203 ++++++++----- 5 files changed, 226 insertions(+), 358 deletions(-) diff --git a/src/app/directives/graphiteSegment.js b/src/app/directives/graphiteSegment.js index 577bbf5d6b7..c8ad131e6c7 100644 --- a/src/app/directives/graphiteSegment.js +++ b/src/app/directives/graphiteSegment.js @@ -37,12 +37,14 @@ function (angular, app, _, $) { if (selected) { segment.value = selected.value; segment.html = selected.html; + segment.fake = false; segment.expandable = selected.expandable; } else { segment.value = value; segment.html = $sce.trustAsHtml(value); segment.expandable = true; + segment.fake = false; } $scope.segmentValueChanged(segment, $scope.$index); }); @@ -71,7 +73,7 @@ function (angular, app, _, $) { options = _.map($scope.altSegments, function(alt) { return alt.value; }); // add custom values - if (segment.value !== 'select metric' && _.indexOf(options, segment.value) === -1) { + if (!segment.fake && _.indexOf(options, segment.value) === -1) { options.unshift(segment.value); } diff --git a/src/app/plugins/datasource/graphite/queryCtrl.js b/src/app/plugins/datasource/graphite/queryCtrl.js index d878386d461..069fa815797 100644 --- a/src/app/plugins/datasource/graphite/queryCtrl.js +++ b/src/app/plugins/datasource/graphite/queryCtrl.js @@ -113,7 +113,7 @@ function (angular, _, config, gfunc, Parser) { function checkOtherSegments(fromIndex) { if (fromIndex === 0) { - $scope.segments.push(new MetricSegment('select metric')); + $scope.segments.push(MetricSegment.newSelectMetric()); return; } @@ -123,13 +123,13 @@ function (angular, _, config, gfunc, Parser) { if (segments.length === 0) { if (path !== '') { $scope.segments = $scope.segments.splice(0, fromIndex); - $scope.segments.push(new MetricSegment('select metric')); + $scope.segments.push(MetricSegment.newSelectMetric()); } return; } if (segments[0].expandable) { if ($scope.segments.length === fromIndex) { - $scope.segments.push(new MetricSegment('select metric')); + $scope.segments.push(MetricSegment.newSelectMetric()); } else { return checkOtherSegments(fromIndex + 1); @@ -238,7 +238,7 @@ function (angular, _, config, gfunc, Parser) { $scope.moveAliasFuncLast(); $scope.smartlyHandleNewAliasByNode(newFunc); - if ($scope.segments.length === 1 && $scope.segments[0].value === 'select metric') { + if ($scope.segments.length === 1 && $scope.segments[0].fake) { $scope.segments = []; } @@ -298,18 +298,17 @@ function (angular, _, config, gfunc, Parser) { return; } - if (_.isString(options)) { - this.value = options; - this.html = $sce.trustAsHtml(this.value); - return; - } - + this.fake = options.fake; this.value = options.value; this.type = options.type; this.expandable = options.expandable; this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value)); } + MetricSegment.newSelectMetric = function() { + return new MetricSegment({value: 'select metric', fake: true}); + }; + }); module.directive('focusMe', function($timeout, $parse) { diff --git a/src/app/plugins/datasource/influxdb/datasource.js b/src/app/plugins/datasource/influxdb/datasource.js index 26bacfe2c59..dbaf443affd 100644 --- a/src/app/plugins/datasource/influxdb/datasource.js +++ b/src/app/plugins/datasource/influxdb/datasource.js @@ -36,7 +36,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { var timeFilter = getTimeFilter(options); var promises = _.map(options.targets, function(target) { - if (target.hide || !((target.series && target.column) || target.query)) { + if (target.hide || !target.query) { return []; } @@ -73,40 +73,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { }); }; - InfluxDatasource.prototype.listColumns = function(seriesName) { - seriesName = templateSrv.replace(seriesName); - - if(!seriesName.match('^/.*/') && !seriesName.match(/^merge\(.*\)/)) { - seriesName = '"' + seriesName+ '"'; - } - - return this._seriesQuery('select * from ' + seriesName + ' limit 1').then(function(data) { - if (!data) { - return []; - } - return data[0].columns.map(function(item) { - return /^\w+$/.test(item) ? item : ('"' + item + '"'); - }); - }); - }; - - InfluxDatasource.prototype.listSeries = function(query) { - // wrap in regex - if (query && query.length > 0 && query[0] !== '/') { - query = '/' + query + '/'; - } - - return this._seriesQuery('SHOW MEASUREMENTS').then(function(data) { - if (!data || data.length === 0) { - return []; - } - return _.map(data[0].points, function(point) { - return point[1]; - }); - }); - }; - - InfluxDatasource.prototype.metricFindQuery = function (query) { + InfluxDatasource.prototype.metricFindQuery = function (query, queryType) { var interpolated; try { interpolated = templateSrv.replace(query); @@ -115,17 +82,30 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { return $q.reject(err); } - return this._seriesQuery(interpolated) - .then(function (results) { - if (!results || results.length === 0) { return []; } + console.log('metricFindQuery called with: ' + [query, queryType].join(', ')); - return _.map(results[0].points, function (metric) { - return { - text: metric[1], - expandable: false - }; - }); - }); + return this._seriesQuery(interpolated, queryType).then(function (results) { + if (!results || results.results.length === 0) { return []; } + + var influxResults = results.results[0]; + if (!influxResults.series) { + return []; + } + + console.log('metric find query response', results); + var series = influxResults.series[0]; + + switch (queryType) { + case 'MEASUREMENTS': + return _.map(series.values, function(value) { return { text: value[0], expandable: true }; }); + case 'TAG_KEYS': + var tagKeys = _.flatten(series.values); + return _.map(tagKeys, function(tagKey) { return { text: tagKey, expandable: true }; }); + case 'TAG_VALUES': + var tagValues = _.flatten(series.values); + return _.map(tagValues, function(tagValue) { return { text: tagValue, expandable: true }; }); + } + }); }; function retry(deferred, callback, delay) { @@ -143,9 +123,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { } InfluxDatasource.prototype._seriesQuery = function(query) { - return this._influxRequest('GET', '/query', { - q: query, - }); + return this._influxRequest('GET', '/query', {q: query}); }; InfluxDatasource.prototype._influxRequest = function(method, url, data) { diff --git a/src/app/plugins/datasource/influxdb/partials/query.editor.html b/src/app/plugins/datasource/influxdb/partials/query.editor.html index d3d7d0ff95d..c91f13c57c6 100644 --- a/src/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/src/app/plugins/datasource/influxdb/partials/query.editor.html @@ -1,18 +1,47 @@
    -
    -
    -
    + +
    +
    -
    -
    -
      -
    • - -
    • -
    • - group by time -
    • -
    • - -
    • -
    • - -
    • -
    -
    -
    - - -
    -
    - -
    -
    Alias patterns
    -
      -
    • $s = series name
    • -
    • $g = group by
    • -
    • $[0-9] part of series name for series names seperated by dots.
    • -
    -
    - -
    -
    Stacking and fill
    -
      -
    • When stacking is enabled it important that points align
    • -
    • If there are missing points for one series it can cause gaps or missing bars
    • -
    • You must use fill(0), and select a group by time low limit
    • -
    • Use the group by time option below your queries and specify for example >10s if your metrics are written every 10 seconds
    • -
    • This will insert zeros for series that are missing measurements and will make stacking work properly
    • -
    -
    - -
    -
    Group by time
    -
      -
    • Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana
    • -
    • Leave the group by time field empty for each query and it will be calculated based on time range and pixel width of the graph
    • -
    • If you use fill(0) or fill(null) set a low limit for the auto group by time interval
    • -
    • The low limit can only be set in the group by time option below your queries
    • -
    • You set a low limit by adding a greater sign before the interval
    • -
    • Example: >60s if you write metrics to InfluxDB every 60 seconds
    • -
    -
    - -
    - - diff --git a/src/app/plugins/datasource/influxdb/queryCtrl.js b/src/app/plugins/datasource/influxdb/queryCtrl.js index 608b5845d88..517ed79e1a1 100644 --- a/src/app/plugins/datasource/influxdb/queryCtrl.js +++ b/src/app/plugins/datasource/influxdb/queryCtrl.js @@ -7,93 +7,23 @@ function (angular, _) { var module = angular.module('grafana.controllers'); - var seriesList = null; - - module.controller('InfluxQueryCtrl', function($scope, $timeout) { + module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q) { $scope.init = function() { - var target = $scope.target; + $scope.segments = $scope.target.segments || []; - target.function = target.function || 'mean'; - target.column = target.column || 'value'; - - // backward compatible correction of schema - if (target.condition_value) { - target.condition = target.condition_key + ' ' + target.condition_op + ' ' + target.condition_value; - delete target.condition_key; - delete target.condition_op; - delete target.condition_value; - } - - if (target.groupby_field_add === false) { - target.groupby_field = ''; - delete target.groupby_field_add; - } - - $scope.rawQuery = true; - - $scope.functions = [ + $scope.functionsSelect = [ 'count', 'mean', 'sum', 'min', 'max', 'mode', 'distinct', 'median', 'derivative', 'stddev', 'first', 'last', 'difference' ]; - $scope.operators = ['=', '=~', '>', '<', '!~', '<>']; - $scope.oldSeries = target.series; - $scope.$on('typeahead-updated', function() { - $timeout($scope.get_data); - }); + checkOtherSegments(0); }; - $scope.showQuery = function () { - $scope.target.rawQuery = true; - }; - - $scope.hideQuery = function () { - $scope.target.rawQuery = false; - }; - - // Cannot use typeahead and ng-change on blur at the same time - $scope.seriesBlur = function() { - if ($scope.oldSeries !== $scope.target.series) { - $scope.oldSeries = $scope.target.series; - $scope.columnList = null; - $scope.get_data(); - } - }; - - $scope.changeFunction = function(func) { - $scope.target.function = func; - $scope.get_data(); - }; - - // called outside of digest - $scope.listColumns = function(query, callback) { - if (!$scope.columnList) { - $scope.$apply(function() { - $scope.datasource.listColumns($scope.target.series).then(function(columns) { - $scope.columnList = columns; - callback(columns); - }); - }); - } - else { - return $scope.columnList; - } - }; - - $scope.listSeries = function(query, callback) { - if (query !== '') { - seriesList = []; - $scope.datasource.listSeries(query).then(function(series) { - seriesList = series; - callback(seriesList); - }); - } - else { - return seriesList; - } + $scope.toggleQueryMode = function () { + $scope.target.rawQuery = !$scope.target.rawQuery; }; $scope.moveMetricQuery = function(fromIndex, toIndex) { @@ -105,6 +35,127 @@ function (angular, _) { $scope.panel.targets.push(clone); }; + $scope.getAltSegments = function (index) { + $scope.altSegments = []; + + var measurement = $scope.segments[0].value; + var queryType, query; + if (index === 0) { + queryType = 'MEASUREMENTS'; + query = 'SHOW MEASUREMENTS'; + } else if (index % 2 === 1) { + queryType = 'TAG_KEYS'; + query = 'SHOW TAG KEYS FROM ' + measurement; + } else { + queryType = 'TAG_VALUES'; + query = "SHOW TAG VALUES FROM " + measurement + " WITH KEY = " + $scope.segments[$scope.segments.length - 2].value; + } + + console.log('getAltSegments: query' , query); + + return $scope.datasource.metricFindQuery(query, queryType).then(function(results) { + console.log('get alt segments: response', results); + $scope.altSegments = _.map(results, function(segment) { + return new MetricSegment({ value: segment.text, expandable: segment.expandable }); + }); + + _.each(templateSrv.variables, function(variable) { + $scope.altSegments.unshift(new MetricSegment({ + type: 'template', + value: '$' + variable.name, + expandable: true, + })); + }); + }, function(err) { + $scope.parserError = err.message || 'Failed to issue metric query'; + }); + }; + + $scope.segmentValueChanged = function (segment, segmentIndex) { + delete $scope.parserError; + + if (segment.expandable) { + return checkOtherSegments(segmentIndex + 1).then(function () { + setSegmentFocus(segmentIndex + 1); + $scope.targetChanged(); + }); + } + else { + $scope.segments = $scope.segments.splice(0, segmentIndex + 1); + } + + setSegmentFocus(segmentIndex + 1); + $scope.targetChanged(); + }; + + $scope.targetChanged = function() { + if ($scope.parserError) { + return; + } + + $scope.$parent.get_data(); + }; + + function checkOtherSegments(fromIndex) { + if (fromIndex === 0) { + $scope.segments.push(MetricSegment.newSelectMetric()); + return; + } + + if ($scope.segments.length === 0) { + throw('should always have a scope segment?'); + } + + if (_.last($scope.segments).fake) { + return $q.when([]); + } else if ($scope.segments.length % 2 === 1) { + $scope.segments.push(MetricSegment.newSelectTag()); + return $q.when([]); + } else { + $scope.segments.push(MetricSegment.newSelectTagValue()); + return $q.when([]); + } + } + + function setSegmentFocus(segmentIndex) { + _.each($scope.segments, function(segment, index) { + segment.focus = segmentIndex === index; + }); + } + + function MetricSegment(options) { + if (options === '*' || options.value === '*') { + this.value = '*'; + this.html = $sce.trustAsHtml(''); + this.expandable = true; + return; + } + + if (_.isString(options)) { + this.value = options; + this.html = $sce.trustAsHtml(this.value); + return; + } + + this.fake = options.fake; + this.value = options.value; + this.type = options.type; + this.expandable = options.expandable; + this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value)); + } + + MetricSegment.newSelectMetric = function() { + return new MetricSegment({value: 'select metric', fake: true}); + }; + + MetricSegment.newSelectTag = function() { + return new MetricSegment({value: 'select tag', fake: true}); + }; + + MetricSegment.newSelectTagValue = function() { + return new MetricSegment({value: 'select tag value', fake: true}); + }; + }); }); From 1f6d5bfd530ae4d367cd30e30d46b2f86ab8254c Mon Sep 17 00:00:00 2001 From: tuexss Date: Wed, 25 Mar 2015 12:03:20 +0100 Subject: [PATCH 32/64] readme cleanup --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index dc9ed582fda..517ec0d4ac8 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [IRC](http://webchat.freenode.net/?channels=grafana) | [Email](mailto:contact@grafana.org) -Grafana is An open source, feature rich metrics dashboard and graph editor for +Grafana is an open source, feature rich metrics dashboard and graph editor for Graphite, InfluxDB & OpenTSDB. ![](http://grafana.org/assets/img/start_page_bg.png) @@ -25,13 +25,13 @@ The code is available in the [develop](https://github.com/grafana/grafana/tree/d - [See it in action](http://grafana.org/docs/features/graphite) ### Graphing -- Fast rendering, even over large timespans. -- Click and drag to zoom. -- Multiple Y-axis. -- Bars, Lines, Points. +- Fast rendering, even over large timespans +- Click and drag to zoom +- Multiple Y-axis +- Bars, Lines, Points - Smart Y-axis formating - Series toggles & color selector -- Legend values, and formating options +- Legend values, and formatting options - Grid thresholds, axis labels - [Annotations](http://grafana.org/docs/features/annotations) @@ -48,7 +48,7 @@ The code is available in the [develop](https://github.com/grafana/grafana/tree/d - [Time range controls](http://grafana.org/docs/features/time_range) ### InfluxDB -- Use InfluxDB as a metric data source, annotation source and for dashboard storage +- Use InfluxDB as a metric data source, annotation source, and for dashboard storage - Query editor with series and column typeahead, easy group by and function selection ### OpenTSDB @@ -62,7 +62,7 @@ There are no dependencies, Grafana is a client side application that runs in you Head to [grafana.org](http://grafana.org) and [download](http://grafana.org/download/) the latest release. -Then follow the quick [setup & config guide](http://grafana.org/docs/). If you have any problems please +Then follow the [quick setup & config guide](http://grafana.org/docs/). If you have any problems please read the [troubleshooting guide](http://grafana.org/docs/troubleshooting). ## Documentation & Support @@ -70,12 +70,12 @@ Be sure to read the [getting started guide](http://grafana.org/docs/features/int feature guides. ## Run from master -Grafana uses nodejs and grunt for asset management (css & javascript), unit test runner and javascript syntax verification. +Grafana uses Node.js and Grunt for asset management (css & javascript), unit test runner and javascript syntax verification. - clone repository - install nodejs - npm install (in project root) - npm install -g grunt-cli -- grunt (runt default task that will generate css files) +- grunt (grunt default task that will generate css files) - grunt build (creates optimized & minified release) - grunt release (same as grunt build but will also create tar & zip package) - grunt test (executes jshint and unit tests) From f235b516dca76516eb43aca3e7b71d0444ccce84 Mon Sep 17 00:00:00 2001 From: tuexss Date: Wed, 25 Mar 2015 12:14:26 +0100 Subject: [PATCH 33/64] http->https for latest version --- latest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/latest.json b/latest.json index 90189fabeca..1ca0904fd94 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { "version": "1.9.1", - "url": "http://grafanarel.s3.amazonaws.com/grafana-1.9.1.tar.gz" + "url": "https://grafanarel.s3.amazonaws.com/grafana-1.9.1.tar.gz" } From 152b01064a7a6ad32bb799c2abde7a6a4667a6eb Mon Sep 17 00:00:00 2001 From: tuexss Date: Wed, 25 Mar 2015 12:17:22 +0100 Subject: [PATCH 34/64] http -> https for external links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dc9ed582fda..04c4d8c6dd4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ [Grafana](http://grafana.org) [![Build Status](https://api.travis-ci.org/grafana/grafana.svg)](https://travis-ci.org/grafana/grafana) [![Coverage Status](https://coveralls.io/repos/grafana/grafana/badge.png)](https://coveralls.io/r/grafana/grafana) [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/grafana/grafana?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ================ [Website](http://grafana.org) | -[Twitter](http://twitter.com/grafana) | -[IRC](http://webchat.freenode.net/?channels=grafana) | +[Twitter](https://twitter.com/grafana) | +[IRC](https://webchat.freenode.net/?channels=grafana) | [Email](mailto:contact@grafana.org) Grafana is An open source, feature rich metrics dashboard and graph editor for From e31a3a64e19cb8896207c1633c75ef5b22d44872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 25 Mar 2015 12:27:33 +0100 Subject: [PATCH 35/64] OpenTSDB: Alias patterns (reference tag values), syntax is: or [[tag_tagname]], Closes #1344, match opentsdb response to query, Fixes #1601 --- CHANGELOG.md | 1 + src/app/features/templating/templateSrv.js | 14 ++++- .../plugins/datasource/opentsdb/datasource.js | 62 ++++++++++++------- .../opentsdb/partials/query.editor.html | 3 +- src/test/specs/templateSrv-specs.js | 16 +++++ 5 files changed, 69 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 690c94a0fbf..15f4fe5fa22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - [Issue #599](https://github.com/grafana/grafana/issues/599). Graph: Added right y axis label setting and graph support - [Issue #1253](https://github.com/grafana/grafana/issues/1253). Graph & Singlestat: Users can now set decimal precision for legend and tooltips (override auto precision) - [Issue #1255](https://github.com/grafana/grafana/issues/1255). Templating: Dashboard will now wait to load until all template variables that have refresh on load set or are initialized via url to be fully loaded and so all variables are in valid state before panels start issuing metric requests. +- [Issue #1344](https://github.com/grafana/grafana/issues/1344). OpenTSDB: Alias patterns (reference tag values), syntax is: $tag_tagname or [[tag_tagname]] **Fixes** - [Issue #1298](https://github.com/grafana/grafana/issues/1298). InfluxDB: Fix handling of empty array in templating variable query diff --git a/src/app/features/templating/templateSrv.js b/src/app/features/templating/templateSrv.js index 8fc45097944..09aed015386 100644 --- a/src/app/features/templating/templateSrv.js +++ b/src/app/features/templating/templateSrv.js @@ -63,13 +63,18 @@ function (angular, _) { }); }; - this.replace = function(target) { + this.replace = function(target, scopedVars) { if (!target) { return; } var value; this._regex.lastIndex = 0; return target.replace(this._regex, function(match, g1, g2) { + if (scopedVars) { + value = scopedVars[g1 || g2]; + if (value) { return value.value; } + } + value = self._values[g1 || g2]; if (!value) { return match; } @@ -77,7 +82,7 @@ function (angular, _) { }); }; - this.replaceWithText = function(target) { + this.replaceWithText = function(target, scopedVars) { if (!target) { return; } var value; @@ -85,6 +90,11 @@ function (angular, _) { this._regex.lastIndex = 0; return target.replace(this._regex, function(match, g1, g2) { + if (scopedVars) { + var option = scopedVars[g1 || g2]; + if (option) { return option.text; } + } + value = self._values[g1 || g2]; text = self._texts[g1 || g2]; if (!value) { return match; } diff --git a/src/app/plugins/datasource/opentsdb/datasource.js b/src/app/plugins/datasource/opentsdb/datasource.js index 3ade07a7aac..cd0c83b7c1d 100644 --- a/src/app/plugins/datasource/opentsdb/datasource.js +++ b/src/app/plugins/datasource/opentsdb/datasource.js @@ -46,13 +46,14 @@ function (angular, _, kbn) { }); }); - return this.performTimeSeriesQuery(queries, start, end) - .then(_.bind(function(response) { - var result = _.map(response.data, _.bind(function(metricData, index) { - return transformMetricData(metricData, groupByTags, this.targets[index]); - }, this)); - return { data: result }; - }, options)); + return this.performTimeSeriesQuery(queries, start, end).then(function(response) { + var metricToTargetMapping = mapMetricsToTargets(response.data, options.targets); + var result = _.map(response.data, function(metricData, index) { + index = metricToTargetMapping[index]; + return transformMetricData(metricData, groupByTags, options.targets[index]); + }); + return { data: result }; + }); }; OpenTSDBDatasource.prototype.performTimeSeriesQuery = function(queries, start, end) { @@ -90,19 +91,8 @@ function (angular, _, kbn) { }; function transformMetricData(md, groupByTags, options) { - var dps = [], - tagData = [], - metricLabel = null; - - if (!_.isEmpty(md.tags)) { - _.each(_.pairs(md.tags), function(tag) { - if (_.has(groupByTags, tag[0])) { - tagData.push(tag[0] + "=" + tag[1]); - } - }); - } - - metricLabel = createMetricLabel(md.metric, tagData, options); + var metricLabel = createMetricLabel(md, options, groupByTags); + var dps = []; // TSDB returns datapoints has a hash of ts => value. // Can't use _.pairs(invert()) because it stringifies keys/values @@ -113,16 +103,31 @@ function (angular, _, kbn) { return { target: metricLabel, datapoints: dps }; } - function createMetricLabel(metric, tagData, options) { + function createMetricLabel(md, options, groupByTags) { if (!_.isUndefined(options) && options.alias) { - return options.alias; + var scopedVars = {}; + _.each(md.tags, function(value, key) { + scopedVars['tag_' + key] = {value: value}; + }); + return templateSrv.replace(options.alias, scopedVars); + } + + var label = md.metric; + var tagData = []; + + if (!_.isEmpty(md.tags)) { + _.each(_.pairs(md.tags), function(tag) { + if (_.has(groupByTags, tag[0])) { + tagData.push(tag[0] + "=" + tag[1]); + } + }); } if (!_.isEmpty(tagData)) { - metric += "{" + tagData.join(", ") + "}"; + label += "{" + tagData.join(", ") + "}"; } - return metric; + return label; } function convertTargetToQuery(target, interval) { @@ -174,6 +179,15 @@ function (angular, _, kbn) { return query; } + function mapMetricsToTargets(metrics, targets) { + return _.map(metrics, function(metricData) { + return _.findIndex(targets, function(target) { + return target.metric === metricData.metric && + _.all(target.tags, function(tagV, tagK) { return metricData.tags[tagK] !== void 0; }); + }); + }); + } + function convertToTSDBTime(date) { if (date === 'now') { return null; diff --git a/src/app/plugins/datasource/opentsdb/partials/query.editor.html b/src/app/plugins/datasource/opentsdb/partials/query.editor.html index 79dd6cd5ffd..a5478ff0cc3 100644 --- a/src/app/plugins/datasource/opentsdb/partials/query.editor.html +++ b/src/app/plugins/datasource/opentsdb/partials/query.editor.html @@ -81,10 +81,11 @@
  • Alias: + Use patterns like $tag_tagname to replace part of the alias for a tag value
  • Date: Wed, 25 Mar 2015 13:43:52 +0100 Subject: [PATCH 36/64] Removed ghost panel --- src/app/features/dashboard/rowCtrl.js | 18 ------------------ src/app/partials/dashboard.html | 8 -------- 2 files changed, 26 deletions(-) diff --git a/src/app/features/dashboard/rowCtrl.js b/src/app/features/dashboard/rowCtrl.js index e2ab9a0c156..494f10657fc 100644 --- a/src/app/features/dashboard/rowCtrl.js +++ b/src/app/features/dashboard/rowCtrl.js @@ -169,22 +169,4 @@ function (angular, app, _, config) { }; }); - module.directive('panelGhost', function() { - return function(scope, element) { - function updateWidth() { - var spanLeft = 12 - scope.dashboard.rowSpan(scope.row); - if (spanLeft > 1) { - element.show(); - element.find('.panel-container').css('height', scope.row.height); - element[0].style.width = ((spanLeft / 1.2) * 10) + '%'; - } else { - element.hide(); - } - } - - updateWidth(); - scope.$on('dashboard-panel-span-updated', updateWidth); - }; - }); - }); diff --git a/src/app/partials/dashboard.html b/src/app/partials/dashboard.html index edfe1bfcafb..39e05ebecb6 100644 --- a/src/app/partials/dashboard.html +++ b/src/app/partials/dashboard.html @@ -86,14 +86,6 @@
  • -
    -
    -
    - -
    -
    -
    -
    From 10618637e2620603635239c37dd8569ad2782010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 25 Mar 2015 13:53:58 +0100 Subject: [PATCH 37/64] Fixed bug in sql migration, closes #1643 --- pkg/services/sqlstore/migrations/datasource_mig.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/migrations/datasource_mig.go b/pkg/services/sqlstore/migrations/datasource_mig.go index 924e1a16189..4f046b1f8e9 100644 --- a/pkg/services/sqlstore/migrations/datasource_mig.go +++ b/pkg/services/sqlstore/migrations/datasource_mig.go @@ -95,5 +95,5 @@ func addDataSourceMigration(mg *Migrator) { "updated": "updated", })) - mg.AddMigration("Drop old table data_source_v1", NewDropTableMigration("data_source_old")) + mg.AddMigration("Drop old table data_source_v1 #2", NewDropTableMigration("data_source_v1")) } From 9c5e116d09abea0e1d27c8b39521db223c1bc861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 25 Mar 2015 14:14:45 +0100 Subject: [PATCH 38/64] Fixed small file nameing issue in build script --- build.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.go b/build.go index f7d77129d92..4ee59928d67 100644 --- a/build.go +++ b/build.go @@ -90,8 +90,8 @@ func main() { func makeLatestDistCopies() { runError("cp", "dist/grafana_"+version+"_amd64.deb", "dist/grafana_latest_amd64.deb") - runError("cp", "dist/grafana-"+strings.Replace(version, "-", "_", 5)+"-1.x86_64.rpm", "dist/grafana-latest-1.x84_64.rpm") - runError("cp", "dist/grafana-"+version+".x86_64.tar.gz", "dist/grafana-latest.x84_64.tar.gz") + runError("cp", "dist/grafana-"+strings.Replace(version, "-", "_", 5)+"-1.x86_64.rpm", "dist/grafana-latest-1.x86_64.rpm") + runError("cp", "dist/grafana-"+version+".x86_64.tar.gz", "dist/grafana-latest.x86_64.tar.gz") } func readVersionFromPackageJson() { From 2e6d28027ad5f6edcbce942fb9db6d6416718e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 25 Mar 2015 14:19:14 +0100 Subject: [PATCH 39/64] Removed snapshot from dashboard settings dropdown, its only reached through the share menu --- src/app/partials/dashboard_topnav.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/partials/dashboard_topnav.html b/src/app/partials/dashboard_topnav.html index 77cccecb10b..bf3ba635a97 100644 --- a/src/app/partials/dashboard_topnav.html +++ b/src/app/partials/dashboard_topnav.html @@ -40,7 +40,6 @@
  • View JSON
  • Save As...
  • Delete dashboard
  • -
  • Snapshot dashboard
  • From cb3593e4725bc2efcbc6c25eddcf7731ca163fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 25 Mar 2015 15:48:51 +0100 Subject: [PATCH 40/64] Lots of small fixes, role viewer hides save icon and some actions in config dropdown. Snapshot dashboard hides save, star, config menu icons. Can now embedd panel from snapshotted dashboard. --- pkg/api/dashboard_snapshot.go | 2 +- src/app/features/dashboard/dashboardCtrl.js | 30 ++++++++++++++++++- src/app/features/dashboard/dashboardSrv.js | 1 + src/app/features/dashboard/sharePanelCtrl.js | 12 +++++--- .../features/dashboard/shareSnapshotCtrl.js | 15 ++++++---- src/app/features/panel/panelHelper.js | 1 + src/app/features/panel/soloPanelCtrl.js | 19 ++++++++---- src/app/partials/dashboard_topnav.html | 16 +++++----- src/app/routes/all.js | 14 +++++---- src/test/specs/soloPanelCtrl-specs.js | 2 +- 10 files changed, 80 insertions(+), 32 deletions(-) diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index c4035e921d5..7af9d8b1f66 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -29,7 +29,7 @@ func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapsho metrics.M_Api_Dashboard_Snapshot_Create.Inc(1) - c.JSON(200, util.DynMap{"key": cmd.Key, "url": setting.ToAbsUrl("/dashboard/snapshots")}) + c.JSON(200, util.DynMap{"key": cmd.Key, "url": setting.ToAbsUrl("/dashboard/snapshot")}) } func createExternalSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) { diff --git a/src/app/features/dashboard/dashboardCtrl.js b/src/app/features/dashboard/dashboardCtrl.js index 8430ac18631..430363574c8 100644 --- a/src/app/features/dashboard/dashboardCtrl.js +++ b/src/app/features/dashboard/dashboardCtrl.js @@ -17,6 +17,7 @@ function (angular, $, config) { templateValuesSrv, dashboardSrv, dashboardViewStateSrv, + contextSrv, $timeout) { $scope.editor = { index: 0 }; @@ -46,7 +47,7 @@ function (angular, $, config) { templateValuesSrv.init(dashboard).then(function() { $scope.dashboard = dashboard; $scope.dashboardViewState = dashboardViewStateSrv.create($scope); - $scope.dashboardMeta = data.meta; + $scope.initDashboardMeta(data.meta, $scope.dashboard); dashboardKeybindings.shortcuts($scope); @@ -57,6 +58,32 @@ function (angular, $, config) { }); }; + $scope.initDashboardMeta = function(meta, dashboard) { + meta.canShare = true; + meta.canSave = true; + meta.canEdit = true; + meta.canStar = true; + + if (contextSrv.hasRole('Viewer')) { + meta.canSave = false; + } + + if (meta.isHome) { + meta.canShare = false; + meta.canStar = false; + meta.canSave = false; + meta.canEdit = false; + } + + if (dashboard.snapshot) { + meta.canEdit = false; + meta.canSave = false; + meta.canStar = false; + } + + $scope.dashboardMeta = meta; + }; + $scope.updateSubmenuVisibility = function() { $scope.submenuEnabled = $scope.dashboard.hasTemplateVarsOrAnnotations(); }; @@ -132,4 +159,5 @@ function (angular, $, config) { }; }); + }); diff --git a/src/app/features/dashboard/dashboardSrv.js b/src/app/features/dashboard/dashboardSrv.js index 77b19b3edd2..20f04b421d7 100644 --- a/src/app/features/dashboard/dashboardSrv.js +++ b/src/app/features/dashboard/dashboardSrv.js @@ -37,6 +37,7 @@ function (angular, $, kbn, _, moment) { this.templating = this._ensureListExist(data.templating); this.annotations = this._ensureListExist(data.annotations); this.refresh = data.refresh; + this.snapshot = data.snapshot; this.schemaVersion = data.schemaVersion || 0; this.version = data.version || 0; diff --git a/src/app/features/dashboard/sharePanelCtrl.js b/src/app/features/dashboard/sharePanelCtrl.js index c7303ab1a68..eb0a7a7a957 100644 --- a/src/app/features/dashboard/sharePanelCtrl.js +++ b/src/app/features/dashboard/sharePanelCtrl.js @@ -71,12 +71,16 @@ function (angular, _, require, config) { } }); - $scope.shareUrl = baseUrl + "?" + paramsArray.join('&'); + var queryParams = "?" + paramsArray.join('&'); + $scope.shareUrl = baseUrl + queryParams; - $scope.soloUrl = $scope.shareUrl.replace('/dashboard/db/', '/dashboard/solo/'); - $scope.iframeHtml = ''; + var soloUrl = $scope.shareUrl; + soloUrl = soloUrl.replace('/dashboard/db/', '/dashboard/solo/db/'); + soloUrl = soloUrl.replace('/dashboard/snapshot/', '/dashboard/solo/snapshot/'); - $scope.imageUrl = $scope.shareUrl.replace('/dashboard/db/', '/render/dashboard/solo/'); + $scope.iframeHtml = ''; + + $scope.imageUrl = soloUrl.replace('/dashboard/', '/render/dashboard/'); $scope.imageUrl += '&width=1000'; $scope.imageUrl += '&height=500'; }; diff --git a/src/app/features/dashboard/shareSnapshotCtrl.js b/src/app/features/dashboard/shareSnapshotCtrl.js index a3f279b9c64..5c41b0ee649 100644 --- a/src/app/features/dashboard/shareSnapshotCtrl.js +++ b/src/app/features/dashboard/shareSnapshotCtrl.js @@ -12,14 +12,17 @@ function (angular) { name: $scope.dashboard.title }; - $scope.createSnapshot = function(makePublic) { - $scope.dashboard.snapshot = true; + $scope.createSnapshot = function(external) { + $scope.dashboard.snapshot = { + timestamp: new Date() + }; + $scope.loading = true; $rootScope.$broadcast('refresh'); $timeout(function() { - $scope.saveSnapshot(makePublic); - }, 2000); + $scope.saveSnapshot(external); + }, 3000); }; $scope.saveSnapshot = function(external) { @@ -35,7 +38,7 @@ function (angular) { }); // cleanup snapshotData - $scope.dashboard.snapshot = false; + delete $scope.dashboard.snapshot; $scope.dashboard.forEachPanel(function(panel) { delete panel.snapshotData; }); @@ -47,7 +50,7 @@ function (angular) { $scope.snapshotUrl = results.url; } else { var baseUrl = $location.absUrl().replace($location.url(), ""); - $scope.snapshotUrl = baseUrl + '/dashboard/snapshots/' + results.key; + $scope.snapshotUrl = baseUrl + '/dashboard/snapshot/' + results.key; } }, function() { $scope.loading = false; diff --git a/src/app/features/panel/panelHelper.js b/src/app/features/panel/panelHelper.js index 62982f69438..442bcbac8c5 100644 --- a/src/app/features/panel/panelHelper.js +++ b/src/app/features/panel/panelHelper.js @@ -8,6 +8,7 @@ function (angular, _, kbn, $) { 'use strict'; var module = angular.module('grafana.services'); + module.service('panelHelper', function(timeSrv) { this.updateTimeRange = function(scope) { diff --git a/src/app/features/panel/soloPanelCtrl.js b/src/app/features/panel/soloPanelCtrl.js index c6a01d9ccfe..3c9a3dc34f0 100644 --- a/src/app/features/panel/soloPanelCtrl.js +++ b/src/app/features/panel/soloPanelCtrl.js @@ -26,12 +26,19 @@ function (angular, $) { var params = $location.search(); panelId = parseInt(params.panelId); - backendSrv.getDashboard($routeParams.slug) - .then(function(dashboard) { - $scope.initPanelScope(dashboard); - }).then(null, function(err) { - $scope.appEvent('alert-error', ['Load panel error', err.message]); - }); + var request; + + if ($routeParams.slug) { + request = backendSrv.getDashboard($routeParams.slug); + } else { + request = backendSrv.get('/api/snapshots/' + $routeParams.key); + } + + request.then(function(dashboard) { + $scope.initPanelScope(dashboard); + }).then(null, function(err) { + $scope.appEvent('alert-error', ['Load panel error', err.message]); + }); }; $scope.initPanelScope = function(dashboard) { diff --git a/src/app/partials/dashboard_topnav.html b/src/app/partials/dashboard_topnav.html index bf3ba635a97..aff1f0bbba7 100644 --- a/src/app/partials/dashboard_topnav.html +++ b/src/app/partials/dashboard_topnav.html @@ -18,19 +18,19 @@
    -
    - - - - - - - - - - -
    From 0122a9ab18febb0606233bfef085fc141acfd39d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 26 Mar 2015 18:03:37 +0100 Subject: [PATCH 57/64] Updated whats new doc --- docs/mkdocs.yml | 2 +- .../{changes_in_v2.md => whats-new-in-v2.md} | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) rename docs/sources/guides/{changes_in_v2.md => whats-new-in-v2.md} (94%) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index bec14f8a9dd..5d4ddbf8de6 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -34,7 +34,7 @@ pages: - ['installation/migrating_to2.md', 'Installation', 'Migrating from v1.x to v2.x'] - ['guides/gettingstarted.md', 'User Guides', 'Getting started'] -- ['guides/changes_in_v2.md', 'User Guides', 'Changes and New Features in v2.0'] +- ['guides/whats-new-in-v2.md', 'User Guides', "What's New in Grafana v2.0"] - ['guides/screencasts.md', 'User Guides', 'Screencasts'] - ['reference/graph.md', 'Reference', 'Graph Panel'] diff --git a/docs/sources/guides/changes_in_v2.md b/docs/sources/guides/whats-new-in-v2.md similarity index 94% rename from docs/sources/guides/changes_in_v2.md rename to docs/sources/guides/whats-new-in-v2.md index 4bab6314991..76701de0a34 100644 --- a/docs/sources/guides/changes_in_v2.md +++ b/docs/sources/guides/whats-new-in-v2.md @@ -1,14 +1,13 @@ --- -page_title: Changes and new features in Grafana v2.0 +page_title: Whats New in Grafana v2.0 page_description: Changes and new features in Grafana v2.0 page_keywords: grafana, changes, features, documentation --- -# Changes and new features in v2.0 +# What's New in Grafana v2.0 This is a guide that descriptes some of changes and new features that can be found in Grafana v2.0. - ## New dashboard top header @@ -19,14 +18,7 @@ This is a guide that descriptes some of changes and new features that can be fou 4. Star/unstar current dashboard 5. Share current dashboard (Make sure the dashboard is saved before) 6. Save current dashboard -7. Settings dropdown - - Dashboard settings - - Annotations - - Templating - - Export (exports current dashboard to json file) - - View JSON (view current dashboard json model) - - Save As... (Copy & Save current dashboard under a new name) - - Delete dashboard +7. Settings dropdown (dashboard settings, annotations, templating, etc) > **Note** In Grafana v2.0 when you change the title of a dashboard and then save it it will no > longer create a new dashboard. It will just change the name for the current dashboard. From 7d0ae23c0e045955e36e706891b79774fb4c2685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 26 Mar 2015 19:31:43 +0100 Subject: [PATCH 58/64] small docs update --- docs/sources/guides/gettingstarted.md | 13 ++++++++++++- docs/sources/guides/whats-new-in-v2.md | 11 +++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/sources/guides/gettingstarted.md b/docs/sources/guides/gettingstarted.md index df936138483..9e0048263f8 100644 --- a/docs/sources/guides/gettingstarted.md +++ b/docs/sources/guides/gettingstarted.md @@ -8,7 +8,18 @@ page_keywords: grafana, guide, documentation This guide will help you get started and acquainted with the Grafana user interface. ## Interface overview - + +### Dashboard header + + +1. Side menu toggle +2. Dashboard title & Search dropdown (also includes access to New dashboard, Import & Playlist) +3. Star/unstar current dashboard +4. Share current dashboard (Make sure the dashboard is saved before) +5. Save current dashboard +6. Settings dropdown (dashboard settings, annotations, templating, etc) + + ## New dashboard ![](/img/animated_gifs/new_dashboard.gif) diff --git a/docs/sources/guides/whats-new-in-v2.md b/docs/sources/guides/whats-new-in-v2.md index 76701de0a34..e78a40d3bcf 100644 --- a/docs/sources/guides/whats-new-in-v2.md +++ b/docs/sources/guides/whats-new-in-v2.md @@ -13,12 +13,11 @@ This is a guide that descriptes some of changes and new features that can be fou 1. Side menu toggle -2. Dashboard search (also includes access to New dashboard, Import & Playlist) -3. Dashboard title -4. Star/unstar current dashboard -5. Share current dashboard (Make sure the dashboard is saved before) -6. Save current dashboard -7. Settings dropdown (dashboard settings, annotations, templating, etc) +2. Dashboard title & Search dropdown (also includes access to New dashboard, Import & Playlist) +3. Star/unstar current dashboard +4. Share current dashboard (Make sure the dashboard is saved before) +5. Save current dashboard +6. Settings dropdown (dashboard settings, annotations, templating, etc) > **Note** In Grafana v2.0 when you change the title of a dashboard and then save it it will no > longer create a new dashboard. It will just change the name for the current dashboard. From 4322f29f34b6bbb7e341012fa068a7b27bc30284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 26 Mar 2015 20:34:58 +0100 Subject: [PATCH 59/64] Dashboard snapshot: added delete key which can be used to delete snapshots, #1623 --- pkg/api/api.go | 2 ++ pkg/api/dashboard_snapshot.go | 28 +++++++++++---- pkg/models/dashboard_snapshot.go | 29 +++++++++++----- pkg/services/sqlstore/dashboard_snapshot.go | 25 ++++++++++---- .../migrations/dashboard_snapshot_mig.go | 34 ++++++++++++++++--- .../dashboard/partials/shareDashboard.html | 3 ++ .../features/dashboard/shareSnapshotCtrl.js | 2 ++ 7 files changed, 97 insertions(+), 26 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 4683e95fa40..88f1a7a37ee 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -44,7 +44,9 @@ func Register(r *macaron.Macaron) { // dashboard snapshots r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot) r.Get("/dashboard/snapshots/*", Index) + r.Get("/api/snapshots/:key", GetDashboardSnapshot) + r.Get("/api/snapshots-delete/:key", DeleteDashboardSnapshot) // authed api r.Group("/api", func() { diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 0f9c918e011..dd9e8f81e5b 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -1,7 +1,6 @@ package api import ( - "strconv" "time" "github.com/grafana/grafana/pkg/api/dtos" @@ -15,12 +14,15 @@ import ( func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) { cmd.Key = util.GetRandomString(32) + cmd.DeleteKey = util.GetRandomString(32) if cmd.External { cmd.OrgId = -1 + cmd.UserId = -1 metrics.M_Api_Dashboard_Snapshot_External.Inc(1) } else { cmd.OrgId = c.OrgId + cmd.UserId = c.UserId metrics.M_Api_Dashboard_Snapshot_Create.Inc(1) } @@ -29,7 +31,12 @@ func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapsho return } - c.JSON(200, util.DynMap{"key": cmd.Key, "url": setting.ToAbsUrl("dashboard/snapshot/" + cmd.Key)}) + c.JSON(200, util.DynMap{ + "key": cmd.Key, + "deleteKey": cmd.DeleteKey, + "url": setting.ToAbsUrl("dashboard/snapshot/" + cmd.Key), + "deleteUrl": setting.ToAbsUrl("api/snapshots-delete/" + cmd.DeleteKey), + }) } func GetDashboardSnapshot(c *middleware.Context) { @@ -58,9 +65,18 @@ func GetDashboardSnapshot(c *middleware.Context) { metrics.M_Api_Dashboard_Snapshot_Get.Inc(1) - maxAge := int64(snapshot.Expires.Sub(time.Now()).Seconds()) - - c.Resp.Header().Set("Cache-Control", "public, max-age="+strconv.FormatInt(maxAge, 10)) - + c.Resp.Header().Set("Cache-Control", "public, max-age=3600") c.JSON(200, dto) } + +func DeleteDashboardSnapshot(c *middleware.Context) { + key := c.Params(":key") + cmd := &m.DeleteDashboardSnapshotCommand{DeleteKey: key} + + if err := bus.Dispatch(cmd); err != nil { + c.JsonApiErr(500, "Failed to delete dashboard snapshot", err) + return + } + + c.JSON(200, util.DynMap{"message": "Snapshot deleted. It might take an hour before it is cleared from a CDN cache."}) +} diff --git a/pkg/models/dashboard_snapshot.go b/pkg/models/dashboard_snapshot.go index cd082666ea9..12638f4150f 100644 --- a/pkg/models/dashboard_snapshot.go +++ b/pkg/models/dashboard_snapshot.go @@ -4,10 +4,14 @@ import "time" // DashboardSnapshot model type DashboardSnapshot struct { - Id int64 - Name string - Key string - OrgId int64 + Id int64 + Name string + Key string + DeleteKey string + OrgId int64 + UserId int64 + External bool + ExternalUrl string Expires time.Time Created time.Time @@ -20,16 +24,23 @@ type DashboardSnapshot struct { // COMMANDS type CreateDashboardSnapshotCommand struct { - Dashboard map[string]interface{} `json:"dashboard" binding:"Required"` - External bool `json:"external"` - Expires int64 `json:"expires"` + Dashboard map[string]interface{} `json:"dashboard" binding:"Required"` + External bool `json:"external"` + ExternalUrl string `json:"externalUrl"` + Expires int64 `json:"expires"` - OrgId int64 `json:"-"` - Key string `json:"-"` + OrgId int64 `json:"-"` + UserId int64 `json:"-"` + Key string `json:"-"` + DeleteKey string `json:"-"` Result *DashboardSnapshot } +type DeleteDashboardSnapshotCommand struct { + DeleteKey string `json:"-"` +} + type GetDashboardSnapshotQuery struct { Key string diff --git a/pkg/services/sqlstore/dashboard_snapshot.go b/pkg/services/sqlstore/dashboard_snapshot.go index 679f679322c..3f66f49f6b6 100644 --- a/pkg/services/sqlstore/dashboard_snapshot.go +++ b/pkg/services/sqlstore/dashboard_snapshot.go @@ -11,6 +11,7 @@ import ( func init() { bus.AddHandler("sql", CreateDashboardSnapshot) bus.AddHandler("sql", GetDashboardSnapshot) + bus.AddHandler("sql", DeleteDashboardSnapshot) } func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error { @@ -23,12 +24,16 @@ func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error { } snapshot := &m.DashboardSnapshot{ - Key: cmd.Key, - OrgId: cmd.OrgId, - Dashboard: cmd.Dashboard, - Expires: expires, - Created: time.Now(), - Updated: time.Now(), + Key: cmd.Key, + DeleteKey: cmd.DeleteKey, + OrgId: cmd.OrgId, + UserId: cmd.UserId, + External: cmd.External, + ExternalUrl: cmd.ExternalUrl, + Dashboard: cmd.Dashboard, + Expires: expires, + Created: time.Now(), + Updated: time.Now(), } _, err := sess.Insert(snapshot) @@ -38,6 +43,14 @@ func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error { }) } +func DeleteDashboardSnapshot(cmd *m.DeleteDashboardSnapshotCommand) error { + return inTransaction(func(sess *xorm.Session) error { + var rawSql = "DELETE FROM dashboard_snapshot WHERE delete_key=?" + _, err := sess.Exec(rawSql, cmd.DeleteKey) + return err + }) +} + func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error { snapshot := m.DashboardSnapshot{Key: query.Key} has, err := x.Get(&snapshot) diff --git a/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go b/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go index f08d7a11e7e..4386f07ffd1 100644 --- a/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go @@ -19,12 +19,36 @@ func addDashboardSnapshotMigrations(mg *Migrator) { }, } + // add v4 mg.AddMigration("create dashboard_snapshot table v4", NewAddTableMigration(snapshotV4)) - addTableIndicesMigrations(mg, "v4", snapshotV4) - mg.AddMigration("add org_id to dashboard_snapshot", new(AddColumnMigration). - Table("dashboard_snapshot").Column(&Column{Name: "org_id", Type: DB_BigInt, Nullable: true})) + // drop v4 + addDropAllIndicesMigrations(mg, "v4", snapshotV4) + mg.AddMigration("drop table dashboard_snapshot_v4 #1", NewDropTableMigration("dashboard_snapshot")) - mg.AddMigration("add index org_id to dashboard_snapshot", - NewAddIndexMigration(snapshotV4, &Index{Cols: []string{"org_id"}})) + snapshotV5 := Table{ + Name: "dashboard_snapshot", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "key", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "delete_key", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "org_id", Type: DB_BigInt, Nullable: false}, + {Name: "user_id", Type: DB_BigInt, Nullable: false}, + {Name: "external", Type: DB_Bool, Nullable: false}, + {Name: "external_url", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "dashboard", Type: DB_Text, Nullable: false}, + {Name: "expires", Type: DB_DateTime, Nullable: false}, + {Name: "created", Type: DB_DateTime, Nullable: false}, + {Name: "updated", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"key"}, Type: UniqueIndex}, + {Cols: []string{"delete_key"}, Type: UniqueIndex}, + {Cols: []string{"user_id"}}, + }, + } + + mg.AddMigration("create dashboard_snapshot table v5 #2", NewAddTableMigration(snapshotV5)) + addTableIndicesMigrations(mg, "v5", snapshotV5) } diff --git a/src/app/features/dashboard/partials/shareDashboard.html b/src/app/features/dashboard/partials/shareDashboard.html index f5f466e9917..fefca5a9100 100644 --- a/src/app/features/dashboard/partials/shareDashboard.html +++ b/src/app/features/dashboard/partials/shareDashboard.html @@ -110,6 +110,9 @@
    +
    diff --git a/src/app/features/dashboard/shareSnapshotCtrl.js b/src/app/features/dashboard/shareSnapshotCtrl.js index 36cd1089f3a..59a0b2ca10a 100644 --- a/src/app/features/dashboard/shareSnapshotCtrl.js +++ b/src/app/features/dashboard/shareSnapshotCtrl.js @@ -84,10 +84,12 @@ function (angular, _) { $scope.loading = false; if (external) { + $scope.deleteUrl = results.deleteUrl; $scope.snapshotUrl = results.url; } else { var baseUrl = $location.absUrl().replace($location.url(), ""); $scope.snapshotUrl = baseUrl + '/dashboard/snapshot/' + results.key; + $scope.deleteUrl = baseUrl + '/api/snapshots-delete/' + results.deleteKey; } $scope.step = 2; From 541cd2e43091be1af739055f478b8f05921c855a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 26 Mar 2015 20:59:41 +0100 Subject: [PATCH 60/64] Dashboard snapshot: more work on snapshot deletion, and saving external reference, #1623 --- pkg/api/dashboard_snapshot.go | 13 ++++++++---- pkg/models/dashboard_snapshot.go | 17 ++++++++-------- pkg/services/sqlstore/dashboard_snapshot.go | 19 +++++++++--------- .../features/dashboard/shareSnapshotCtrl.js | 20 +++++++++++++------ 4 files changed, 41 insertions(+), 28 deletions(-) diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index dd9e8f81e5b..1c641d10c1c 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -13,14 +13,19 @@ import ( ) func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) { - cmd.Key = util.GetRandomString(32) - cmd.DeleteKey = util.GetRandomString(32) - if cmd.External { + // external snapshot ref requires key and delete key + if cmd.Key != "" && cmd.DeleteKey != "" { + c.JsonApiErr(400, "Missing key and delete key for external snapshot", nil) + return + } + cmd.OrgId = -1 cmd.UserId = -1 metrics.M_Api_Dashboard_Snapshot_External.Inc(1) } else { + cmd.Key = util.GetRandomString(32) + cmd.DeleteKey = util.GetRandomString(32) cmd.OrgId = c.OrgId cmd.UserId = c.UserId metrics.M_Api_Dashboard_Snapshot_Create.Inc(1) @@ -78,5 +83,5 @@ func DeleteDashboardSnapshot(c *middleware.Context) { return } - c.JSON(200, util.DynMap{"message": "Snapshot deleted. It might take an hour before it is cleared from a CDN cache."}) + c.JSON(200, util.DynMap{"message": "Snapshot deleted. It might take an hour before it's cleared from a CDN cache."}) } diff --git a/pkg/models/dashboard_snapshot.go b/pkg/models/dashboard_snapshot.go index 12638f4150f..e8f37e2a236 100644 --- a/pkg/models/dashboard_snapshot.go +++ b/pkg/models/dashboard_snapshot.go @@ -24,15 +24,16 @@ type DashboardSnapshot struct { // COMMANDS type CreateDashboardSnapshotCommand struct { - Dashboard map[string]interface{} `json:"dashboard" binding:"Required"` - External bool `json:"external"` - ExternalUrl string `json:"externalUrl"` - Expires int64 `json:"expires"` + Dashboard map[string]interface{} `json:"dashboard" binding:"Required"` + Expires int64 `json:"expires"` - OrgId int64 `json:"-"` - UserId int64 `json:"-"` - Key string `json:"-"` - DeleteKey string `json:"-"` + // these are passed when storing an external snapshot ref + External bool `json:"external"` + Key string `json:"key"` + DeleteKey string `json:"deleteKey"` + + OrgId int64 `json:"-"` + UserId int64 `json:"-"` Result *DashboardSnapshot } diff --git a/pkg/services/sqlstore/dashboard_snapshot.go b/pkg/services/sqlstore/dashboard_snapshot.go index 3f66f49f6b6..0bbb01ed6bd 100644 --- a/pkg/services/sqlstore/dashboard_snapshot.go +++ b/pkg/services/sqlstore/dashboard_snapshot.go @@ -24,16 +24,15 @@ func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error { } snapshot := &m.DashboardSnapshot{ - Key: cmd.Key, - DeleteKey: cmd.DeleteKey, - OrgId: cmd.OrgId, - UserId: cmd.UserId, - External: cmd.External, - ExternalUrl: cmd.ExternalUrl, - Dashboard: cmd.Dashboard, - Expires: expires, - Created: time.Now(), - Updated: time.Now(), + Key: cmd.Key, + DeleteKey: cmd.DeleteKey, + OrgId: cmd.OrgId, + UserId: cmd.UserId, + External: cmd.External, + Dashboard: cmd.Dashboard, + Expires: expires, + Created: time.Now(), + Updated: time.Now(), } _, err := sess.Insert(snapshot) diff --git a/src/app/features/dashboard/shareSnapshotCtrl.js b/src/app/features/dashboard/shareSnapshotCtrl.js index 59a0b2ca10a..240bd33488b 100644 --- a/src/app/features/dashboard/shareSnapshotCtrl.js +++ b/src/app/features/dashboard/shareSnapshotCtrl.js @@ -29,6 +29,9 @@ function (angular, _) { {text: 'Public on the web', value: 3}, ]; + $scope.externalUrl = 'http://snapshots-origin.raintank.io'; + $scope.apiUrl = '/api/snapshots'; + $scope.createSnapshot = function(external) { $scope.dashboard.snapshot = { timestamp: new Date() @@ -71,21 +74,18 @@ function (angular, _) { var cmdData = { dashboard: dash, - external: external === true, expires: $scope.snapshot.expires, }; - var apiUrl = '/api/snapshots/'; - if (external) { - apiUrl = "http://snapshots-origin.raintank.io/api/snapshots"; - } + var postUrl = external ? $scope.externalUrl + $scope.apiUrl : $scope.apiUrl; - backendSrv.post(apiUrl, cmdData).then(function(results) { + backendSrv.post(postUrl, cmdData).then(function(results) { $scope.loading = false; if (external) { $scope.deleteUrl = results.deleteUrl; $scope.snapshotUrl = results.url; + $scope.saveExternalSnapshotRef(cmdData, results); } else { var baseUrl = $location.absUrl().replace($location.url(), ""); $scope.snapshotUrl = baseUrl + '/dashboard/snapshot/' + results.key; @@ -98,6 +98,14 @@ function (angular, _) { }); }; + $scope.saveExternalSnapshotRef = function(cmdData, results) { + // save external in local instance as well + cmdData.external = true; + cmdData.key = results.key; + cmdData.delete_key = results.delete_key; + backendSrv.post('/api/snapshots/', cmdData); + }; + }); }); From 7be7aeb70ad571ca7740c177a24a1a223df82bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 26 Mar 2015 21:20:44 +0100 Subject: [PATCH 61/64] Fixed sql migration issue with dashboard snapshots --- pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go b/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go index 4386f07ffd1..4d83dfd5bc6 100644 --- a/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go @@ -21,9 +21,6 @@ func addDashboardSnapshotMigrations(mg *Migrator) { // add v4 mg.AddMigration("create dashboard_snapshot table v4", NewAddTableMigration(snapshotV4)) - - // drop v4 - addDropAllIndicesMigrations(mg, "v4", snapshotV4) mg.AddMigration("drop table dashboard_snapshot_v4 #1", NewDropTableMigration("dashboard_snapshot")) snapshotV5 := Table{ From d3db49ae3ead84c550d8fd0b23d666e1d5cef67d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 27 Mar 2015 06:47:58 +0100 Subject: [PATCH 62/64] Fixed snapshot sharing issue --- pkg/api/dashboard_snapshot.go | 2 +- src/app/features/dashboard/shareSnapshotCtrl.js | 2 +- src/app/services/backendSrv.js | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 1c641d10c1c..8de96ec9f21 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -15,7 +15,7 @@ import ( func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) { if cmd.External { // external snapshot ref requires key and delete key - if cmd.Key != "" && cmd.DeleteKey != "" { + if cmd.Key == "" || cmd.DeleteKey == "" { c.JsonApiErr(400, "Missing key and delete key for external snapshot", nil) return } diff --git a/src/app/features/dashboard/shareSnapshotCtrl.js b/src/app/features/dashboard/shareSnapshotCtrl.js index 240bd33488b..4006abfe4b5 100644 --- a/src/app/features/dashboard/shareSnapshotCtrl.js +++ b/src/app/features/dashboard/shareSnapshotCtrl.js @@ -102,7 +102,7 @@ function (angular, _) { // save external in local instance as well cmdData.external = true; cmdData.key = results.key; - cmdData.delete_key = results.delete_key; + cmdData.deleteKey = results.deleteKey; backendSrv.post('/api/snapshots/', cmdData); }; diff --git a/src/app/services/backendSrv.js b/src/app/services/backendSrv.js index 13565342333..004bf663e86 100644 --- a/src/app/services/backendSrv.js +++ b/src/app/services/backendSrv.js @@ -54,12 +54,16 @@ function (angular, _, config) { this.request = function(options) { var httpOptions = { - url: config.appSubUrl + options.url, + url: options.url, method: options.method, data: options.data, params: options.params, }; + if (httpOptions.url.indexOf('/') === 0) { + httpOptions.url = config.appSubUrl + httpOptions.url; + } + return $http(httpOptions).then(function(results) { if (options.method !== 'GET') { if (results && results.data.message) { From e646ae8be490915b5fcfd8d0824ccb19a61d5979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 27 Mar 2015 07:39:06 +0100 Subject: [PATCH 63/64] updated whats new doc --- docs/sources/guides/whats-new-in-v2.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v2.md b/docs/sources/guides/whats-new-in-v2.md index e78a40d3bcf..bb5c4c15c7d 100644 --- a/docs/sources/guides/whats-new-in-v2.md +++ b/docs/sources/guides/whats-new-in-v2.md @@ -1,12 +1,12 @@ --- -page_title: Whats New in Grafana v2.0 -page_description: Changes and new features in Grafana v2.0 -page_keywords: grafana, changes, features, documentation +page_title: What's New in Grafana v2.0 +page_description: What's new in Grafana v2.0 +page_keywords: grafana, new, changes, features, documentation --- # What's New in Grafana v2.0 -This is a guide that descriptes some of changes and new features that can be found in Grafana v2.0. +This is a guide that describes some of changes and new features that can be found in Grafana v2.0. ## New dashboard top header From 1d64ba3b5d361064db9edd59e867a931ffb4df39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 27 Mar 2015 08:39:08 +0100 Subject: [PATCH 64/64] Small style update to submenu (template variables, annotation menu) --- src/app/partials/submenu.html | 7 +------ src/css/less/submenu.less | 4 ++-- src/css/less/tightform.less | 3 ++- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/app/partials/submenu.html b/src/app/partials/submenu.html index 416fb47558b..fcfb71f5d4f 100644 --- a/src/app/partials/submenu.html +++ b/src/app/partials/submenu.html @@ -3,9 +3,6 @@
      -
    • - VARIABLES: -
    • ${{variable.name}}: @@ -20,11 +17,9 @@