From 72f6dc1e0c23ce6193386c033be9fe06f3a6950e Mon Sep 17 00:00:00 2001 From: Teet Talviste Date: Fri, 6 Feb 2015 18:15:27 +0200 Subject: [PATCH 001/118] enable sending calculated interval to opentsdb downsample --- src/app/features/opentsdb/datasource.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/app/features/opentsdb/datasource.js b/src/app/features/opentsdb/datasource.js index d5bdd011dcf..58578a4d7c2 100644 --- a/src/app/features/opentsdb/datasource.js +++ b/src/app/features/opentsdb/datasource.js @@ -24,7 +24,15 @@ function (angular, _, kbn) { OpenTSDBDatasource.prototype.query = function(options) { var start = convertToTSDBTime(options.range.from); var end = convertToTSDBTime(options.range.to); - var queries = _.compact(_.map(options.targets, convertTargetToQuery)); + var qs = Array(); + if (options.interval.match(/\.[0-9]+s/)) { + options.interval = parseFloat(options.interval)*1000 + "ms" + } + _.each(options.targets, function(target){ + qs.push(convertTargetToQuery(target, options.interval)); + } + ); + var queries = _.compact(qs); // No valid targets, return the empty result to save a round trip. if (_.isEmpty(queries)) { @@ -119,7 +127,7 @@ function (angular, _, kbn) { return metric; } - function convertTargetToQuery(target) { + function convertTargetToQuery(target, interval) { if (!target.metric) { return null; } @@ -149,7 +157,7 @@ function (angular, _, kbn) { } if (target.shouldDownsample) { - query.downsample = templateSrv.replace(target.downsampleInterval) + "-" + target.downsampleAggregator; + query.downsample = templateSrv.replace(target.downsampleInterval ? target.downsampleInterval : interval ) + "-" + target.downsampleAggregator; } query.tags = angular.copy(target.tags); From 28f7806d1994df20051009d65a210fb88402e8c0 Mon Sep 17 00:00:00 2001 From: Teet Talviste Date: Fri, 6 Feb 2015 18:39:05 +0200 Subject: [PATCH 002/118] fix some linting problems --- src/app/features/opentsdb/datasource.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/features/opentsdb/datasource.js b/src/app/features/opentsdb/datasource.js index 58578a4d7c2..c8617d7a383 100644 --- a/src/app/features/opentsdb/datasource.js +++ b/src/app/features/opentsdb/datasource.js @@ -24,9 +24,9 @@ function (angular, _, kbn) { OpenTSDBDatasource.prototype.query = function(options) { var start = convertToTSDBTime(options.range.from); var end = convertToTSDBTime(options.range.to); - var qs = Array(); + var qs = []; if (options.interval.match(/\.[0-9]+s/)) { - options.interval = parseFloat(options.interval)*1000 + "ms" + options.interval = parseFloat(options.interval)*1000 + "ms"; } _.each(options.targets, function(target){ qs.push(convertTargetToQuery(target, options.interval)); @@ -157,7 +157,8 @@ function (angular, _, kbn) { } if (target.shouldDownsample) { - query.downsample = templateSrv.replace(target.downsampleInterval ? target.downsampleInterval : interval ) + "-" + target.downsampleAggregator; + var buf = target.downsampleInterval || interval; + query.downsample = templateSrv.replace(buf) + "-" + target.downsampleAggregator; } query.tags = angular.copy(target.tags); From ddfb36b511be8b6d5d00432118ea12b0d53df55d Mon Sep 17 00:00:00 2001 From: Teet Talviste Date: Fri, 6 Feb 2015 21:07:31 +0200 Subject: [PATCH 003/118] satisfy travis: fix curly brace space --- src/app/features/opentsdb/datasource.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/features/opentsdb/datasource.js b/src/app/features/opentsdb/datasource.js index c8617d7a383..098bd809a2a 100644 --- a/src/app/features/opentsdb/datasource.js +++ b/src/app/features/opentsdb/datasource.js @@ -28,10 +28,9 @@ function (angular, _, kbn) { if (options.interval.match(/\.[0-9]+s/)) { options.interval = parseFloat(options.interval)*1000 + "ms"; } - _.each(options.targets, function(target){ + _.each(options.targets, function(target) { qs.push(convertTargetToQuery(target, options.interval)); - } - ); + }); var queries = _.compact(qs); // No valid targets, return the empty result to save a round trip. From a0a4b04007bb448880c8323048eac07457fc4d26 Mon Sep 17 00:00:00 2001 From: Teet Talviste Date: Fri, 6 Feb 2015 21:15:14 +0200 Subject: [PATCH 004/118] fix identation --- src/app/features/opentsdb/datasource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/features/opentsdb/datasource.js b/src/app/features/opentsdb/datasource.js index 098bd809a2a..a90379b9fc4 100644 --- a/src/app/features/opentsdb/datasource.js +++ b/src/app/features/opentsdb/datasource.js @@ -29,7 +29,7 @@ function (angular, _, kbn) { options.interval = parseFloat(options.interval)*1000 + "ms"; } _.each(options.targets, function(target) { - qs.push(convertTargetToQuery(target, options.interval)); + qs.push(convertTargetToQuery(target, options.interval)); }); var queries = _.compact(qs); From 2de6f3434c7771f0848e15577fa1226571936c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 9 Feb 2015 13:34:12 +0100 Subject: [PATCH 005/118] Fixed default '@timestamp' time field for elasticsearch annotations, Fixes #1444 --- src/app/features/elasticsearch/datasource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/features/elasticsearch/datasource.js b/src/app/features/elasticsearch/datasource.js index 0ca8ec853f1..d12cc5d113d 100644 --- a/src/app/features/elasticsearch/datasource.js +++ b/src/app/features/elasticsearch/datasource.js @@ -68,7 +68,7 @@ function (angular, _, config, kbn, moment) { var titleField = annotation.titleField || 'desc'; var textField = annotation.textField || null; - range[annotation.timeField]= { + range[timeField]= { from: rangeUnparsed.from, to: rangeUnparsed.to, }; From cd8f938cab292e329ac8e50b2fad5e705322fe4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 9 Feb 2015 16:02:42 +0100 Subject: [PATCH 006/118] Fixed issue with sidenav toggle on non dashboard pages --- src/app/directives/topnav.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/directives/topnav.js b/src/app/directives/topnav.js index fb9d8a82ed1..c871dd085cb 100644 --- a/src/app/directives/topnav.js +++ b/src/app/directives/topnav.js @@ -20,7 +20,7 @@ function (angular) { template: '', link: function(scope, elem, attrs) { scope.icon = attrs.icon; - scope.showMenuBtn = !contextSrv.sidemenu; + scope.contextSrv = contextSrv; scope.toggle = function() { $rootScope.appEvent('toggle-sidemenu'); From 5c795e9a50f77e16f5105b90bf0b0d53b9a485da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 9 Feb 2015 16:18:30 +0100 Subject: [PATCH 007/118] Updated readme with docker info, updated build task --- README.md | 5 +++++ tasks/build_task.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 48fc24ce0c7..0e5ae0c7f9c 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,11 @@ Graphite, InfluxDB & OpenTSDB. Grafana 2.0 comes with a backend written in Go. It is not ready for production use yet as there is still a lot of small issues to fix and polish missing. But feedback on what is done and bug reports would be greatly appreciated. +## Try it out with docker +``` +docker run -i -p 3000:3000 grafana/grafana:develop +``` + ## building and running ``` diff --git a/tasks/build_task.js b/tasks/build_task.js index bb175dc3bb3..1f6e072603a 100644 --- a/tasks/build_task.js +++ b/tasks/build_task.js @@ -43,7 +43,7 @@ module.exports = function(grunt) { }); grunt.config('copy.backend_files', { expand: true, - src: ['conf/*', 'vendor/**/*', '!conf/grafana.dev.ini'], + src: ['conf/grafana.ini', 'vendor/**/*'], options: { mode: true}, dest: '<%= tempDir %>' }); From c5afcd8e09c2d9d46a6f4f560ea4d2ea1e831adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 10 Feb 2015 10:19:43 +0100 Subject: [PATCH 008/118] Fixed issue with datasource proxying, it required account admin role when it should not --- pkg/api/api.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index fd7c7283eb4..02994d1047f 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -2,10 +2,10 @@ package api import ( "github.com/Unknwon/macaron" - "github.com/macaron-contrib/binding" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" + "github.com/macaron-contrib/binding" ) // Register adds http routes @@ -72,9 +72,10 @@ func Register(r *macaron.Macaron) { r.Group("/datasources", func() { r.Combo("/").Get(GetDataSources).Put(AddDataSource).Post(UpdateDataSource) r.Delete("/:id", DeleteDataSource) - r.Any("/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest) }, reqAccountAdmin) + r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest) + // Dashboard r.Group("/dashboards", func() { r.Combo("/db/:slug").Get(GetDashboard).Delete(DeleteDashboard) From 088ad881e049134a3f636d3cd6d88fab45a85e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 10 Feb 2015 11:13:35 +0100 Subject: [PATCH 009/118] Updated phantomjs panel rendering code --- vendor/phantomjs/render.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/phantomjs/render.js b/vendor/phantomjs/render.js index f5e17eca527..0746c217b79 100644 --- a/vendor/phantomjs/render.js +++ b/vendor/phantomjs/render.js @@ -31,7 +31,7 @@ page.open(params.url, function (status) { return $('canvas').length > 0; }); - if (canvas || tries === 5) { + if (canvas || tries === 10000) { page.render(params.png); phantom.exit(); } From e165e2af9526ac48c33b7f7121f11b547b4d706d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 10 Feb 2015 15:36:51 +0100 Subject: [PATCH 010/118] Worked on user admin features, can now create and edit users as a grafana admin user, #1446 --- pkg/api/admin_users.go | 77 ++++++++++++++++++- pkg/api/api.go | 3 + pkg/api/dtos/user.go | 14 ++++ pkg/models/user.go | 5 ++ pkg/services/sqlstore/user.go | 18 ++++- src/app/features/admin/adminEditUserCtrl.js | 40 ++++++++++ .../features/admin/partials/edit_user.html | 74 ++++++++++++++++++ src/app/features/admin/partials/users.html | 11 ++- src/app/features/all.js | 1 + .../features/profile/partials/profile.html | 2 +- src/app/routes/backend/all.js | 8 ++ 11 files changed, 246 insertions(+), 7 deletions(-) create mode 100644 pkg/api/dtos/user.go create mode 100644 src/app/features/admin/adminEditUserCtrl.js create mode 100644 src/app/features/admin/partials/edit_user.html diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index b202c5ce2a8..0ffe0612ac0 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -1,6 +1,7 @@ package api import ( + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" @@ -12,9 +13,83 @@ func AdminSearchUsers(c *middleware.Context) { query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 20} if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to fetch collaboratos", err) + c.JsonApiErr(500, "Failed to fetch users", err) return } c.JSON(200, query.Result) } + +func AdminGetUser(c *middleware.Context) { + userId := c.ParamsInt64(":id") + + query := m.GetUserByIdQuery{Id: userId} + + if err := bus.Dispatch(&query); err != nil { + c.JsonApiErr(500, "Failed to fetch user", err) + return + } + + result := m.UserDTO{ + Name: query.Result.Name, + Email: query.Result.Email, + Login: query.Result.Login, + } + + c.JSON(200, result) +} + +func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) { + cmd := m.CreateUserCommand{ + Login: form.Login, + Email: form.Email, + Password: form.Password, + Name: form.Name, + } + + if len(cmd.Login) == 0 { + cmd.Login = cmd.Email + if len(cmd.Login) == 0 { + c.JsonApiErr(400, "Validation error, need specify either username or email", nil) + return + } + } + + if len(cmd.Password) < 4 { + c.JsonApiErr(400, "Password is missing or too short", nil) + return + } + + if err := bus.Dispatch(&cmd); err != nil { + c.JsonApiErr(500, "failed to create user", err) + return + } + + c.JsonOK("User created") +} + +func AdminUpdateUser(c *middleware.Context, form dtos.AdminUpdateUserForm) { + userId := c.ParamsInt64(":id") + + cmd := m.UpdateUserCommand{ + UserId: userId, + Login: form.Login, + Email: form.Email, + Name: form.Name, + } + + if len(cmd.Login) == 0 { + cmd.Login = cmd.Email + if len(cmd.Login) == 0 { + c.JsonApiErr(400, "Validation error, need specify either username or email", nil) + return + } + } + + if err := bus.Dispatch(&cmd); err != nil { + c.JsonApiErr(500, "failed to update user", err) + return + } + + c.JsonOK("User updated") +} diff --git a/pkg/api/api.go b/pkg/api/api.go index 02994d1047f..4595869ab0b 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -93,6 +93,9 @@ func Register(r *macaron.Macaron) { // admin api r.Group("/api/admin", func() { r.Get("/users", AdminSearchUsers) + r.Get("/users/:id", AdminGetUser) + r.Post("/users", bind(dtos.AdminCreateUserForm{}), AdminCreateUser) + r.Put("/users/:id", bind(dtos.AdminUpdateUserForm{}), AdminUpdateUser) }, reqGrafanaAdmin) // rendering diff --git a/pkg/api/dtos/user.go b/pkg/api/dtos/user.go new file mode 100644 index 00000000000..69fc3797029 --- /dev/null +++ b/pkg/api/dtos/user.go @@ -0,0 +1,14 @@ +package dtos + +type AdminCreateUserForm struct { + Email string `json:"email"` + Login string `json:"login"` + Name string `json:"name"` + Password string `json:"password" binding:"Required"` +} + +type AdminUpdateUserForm struct { + Email string `json:"email"` + Login string `json:"login"` + Name string `json:"name"` +} diff --git a/pkg/models/user.go b/pkg/models/user.go index 0a3de0c9a4d..b63ded299c7 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -63,6 +63,11 @@ type GetUserByLoginQuery struct { Result *User } +type GetUserByIdQuery struct { + Id int64 + Result *User +} + type GetSignedInUserQuery struct { UserId int64 Result *SignedInUser diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index f56a64c8c0e..ac7286af77c 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -15,6 +15,7 @@ import ( func init() { bus.AddHandler("sql", CreateUser) + bus.AddHandler("sql", GetUserById) bus.AddHandler("sql", UpdateUser) bus.AddHandler("sql", GetUserByLogin) bus.AddHandler("sql", SetUsingAccount) @@ -112,9 +113,24 @@ func CreateUser(cmd *m.CreateUserCommand) error { }) } +func GetUserById(query *m.GetUserByIdQuery) error { + user := new(m.User) + has, err := x.Id(query.Id).Get(user) + + if err != nil { + return err + } else if has == false { + return m.ErrUserNotFound + } + + query.Result = user + + return nil +} + func GetUserByLogin(query *m.GetUserByLoginQuery) error { if query.LoginOrEmail == "" { - return m.ErrAccountNotFound + return m.ErrUserNotFound } user := new(m.User) diff --git a/src/app/features/admin/adminEditUserCtrl.js b/src/app/features/admin/adminEditUserCtrl.js new file mode 100644 index 00000000000..d01d5d8559d --- /dev/null +++ b/src/app/features/admin/adminEditUserCtrl.js @@ -0,0 +1,40 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('AdminEditUserCtrl', function($scope, $routeParams, backendSrv) { + $scope.user = {}; + + $scope.init = function() { + if ($routeParams.id) { + $scope.createMode = false; + $scope.getUser($routeParams.id); + } else { + $scope.createMode = true; + } + }; + + $scope.getUser = function(id) { + backendSrv.get('/api/admin/users/' + id).then(function(user) { + $scope.user = user; + $scope.user_id = id; + }); + }; + + $scope.update = function() { + if (!$scope.userForm.$valid) { return; } + if ($scope.createMode) { + backendSrv.post('/api/admin/users', $scope.user); + } else { + backendSrv.put('/api/admin/users/' + $scope.user_id, $scope.user); + } + }; + + $scope.init(); + + }); +}); diff --git a/src/app/features/admin/partials/edit_user.html b/src/app/features/admin/partials/edit_user.html new file mode 100644 index 00000000000..4209b280d5b --- /dev/null +++ b/src/app/features/admin/partials/edit_user.html @@ -0,0 +1,74 @@ + + + + +
+
+

+ Create a new user +

+ +

+ Edit user +

+ +
+
+
+
    +
  • + Name +
  • +
  • + +
  • +
+
+
+
+
    +
  • + Email +
  • +
  • + +
  • +
+
+
+
+
    +
  • + Username +
  • +
  • + +
  • +
+
+
+ +
+
    +
  • + Password +
  • +
  • + +
  • +
+
+
+
+ +
+ + +
+
+
diff --git a/src/app/features/admin/partials/users.html b/src/app/features/admin/partials/users.html index ebc31919457..fb59f6ee514 100644 --- a/src/app/features/admin/partials/users.html +++ b/src/app/features/admin/partials/users.html @@ -2,30 +2,33 @@
-

Users

+

+ Users +

+ - + - From 0d0d8025260871abd8157ee4cffc0ab7ed80722d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 11 Feb 2015 21:21:21 +0100 Subject: [PATCH 017/118] Removed data folder stuff, should not be in repo --- data/dashboards/annotations.json | 269 ------ data/dashboards/default.json | 409 --------- data/dashboards/grafana-play-home.json | 1 - data/dashboards/graph-styles.json | 796 ------------------ data/dashboards/graph_styles_collapsed.json | 1 - data/dashboards/home.json | 1 - data/dashboards/templated-graphs-nested.json | 288 ------- data/dashboards/templated-graphs.json | 272 ------ data/dashboards/testing-save.json | 1 - data/dashboards/white-theme.json | 519 ------------ data/png/77dedee4f4d479cea6f9d608353ed098.png | Bin 64800 -> 0 bytes data/png/9e956287bee2bc4eabd91f9bb06ab458.png | Bin 73511 -> 0 bytes 12 files changed, 2557 deletions(-) delete mode 100644 data/dashboards/annotations.json delete mode 100644 data/dashboards/default.json delete mode 100644 data/dashboards/grafana-play-home.json delete mode 100644 data/dashboards/graph-styles.json delete mode 100644 data/dashboards/graph_styles_collapsed.json delete mode 100644 data/dashboards/home.json delete mode 100644 data/dashboards/templated-graphs-nested.json delete mode 100644 data/dashboards/templated-graphs.json delete mode 100644 data/dashboards/testing-save.json delete mode 100644 data/dashboards/white-theme.json delete mode 100644 data/png/77dedee4f4d479cea6f9d608353ed098.png delete mode 100644 data/png/9e956287bee2bc4eabd91f9bb06ab458.png diff --git a/data/dashboards/annotations.json b/data/dashboards/annotations.json deleted file mode 100644 index 727313e95dd..00000000000 --- a/data/dashboards/annotations.json +++ /dev/null @@ -1,269 +0,0 @@ -{ - "title": "Annotations", - "services": { - "filter": { - "list": [], - "time": { - "from": "now-1h", - "to": "now" - } - } - }, - "rows": [ - { - "title": "Welcome to Grafana", - "height": "350px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 12, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 0, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(apps.fakesite.web_server_02.counters.requests.count,2)" - }, - { - "target": "aliasByNode(apps.fakesite.web_server_01.counters.requests.count,2)" - } - ], - "aliasColors": {}, - "aliasYAxis": {}, - "title": "Amnotations example", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - } - ], - "notice": false - }, - { - "title": "test", - "height": "350px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(apps.fakesite.web_server_02.counters.request_status.code_304.count,5)" - } - ], - "aliasColors": { - "web_server_01": "#1F78C1", - "web_server_02": "#6ED0E0" - }, - "aliasYAxis": {}, - "title": "Annotations example", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "### Annotations\n\n- Annotation is a feature that must be enabled in the dashboards settings / Controls tab / Feature toggles\n- Annotation bar is then visible at the top. \n- Click on the cog to open the Annotations dialog \n- In this dialog you can add or edit annotations \n- Currently only Graphite metrics and Graphite events are supported sources of annotations\n- More datasource options for annotations will be added \n- Click on the annotation name in the bar to toggle the annotation on or off\n\n", - "style": {}, - "title": "Description" - } - ], - "notice": false - } - ], - "editable": true, - "failover": false, - "panel_hints": true, - "style": "dark", - "pulldowns": [ - { - "type": "filtering", - "collapse": false, - "notice": false, - "enable": false - }, - { - "type": "annotations", - "enable": true, - "annotations": [ - { - "name": "deploys", - "type": "graphite metric", - "showLine": true, - "iconColor": "#C0C6BE", - "lineColor": "rgba(253, 54, 54, 0.77)", - "iconSize": 13, - "enable": true, - "target": "alias(apps.fakesite.web_server_01.counters.request_status.code_500.count, 'deployed v1.3')" - }, - { - "name": "puppet apply", - "type": "graphite metric", - "showLine": true, - "iconColor": "#C0C6BE", - "lineColor": "rgba(255, 96, 96, 0.592157)", - "iconSize": 13, - "enable": false, - "target": "alias(apps.fakesite.web_server_02.counters.request_status.code_403.count,'puppet apply')" - } - ] - } - ], - "nav": [ - { - "type": "timepicker", - "collapse": false, - "notice": false, - "enable": true, - "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "now": true - } - ], - "loader": { - "save_gist": false, - "save_elasticsearch": true, - "save_local": true, - "save_default": true, - "save_temp": true, - "save_temp_ttl_enable": true, - "save_temp_ttl": "30d", - "load_gist": false, - "load_elasticsearch": true, - "load_elasticsearch_size": 20, - "load_local": false, - "hide": false - }, - "refresh": false, - "tags": [ - "annotations", - "graphite", - "showcase" - ], - "timezone": "browser" -} \ No newline at end of file diff --git a/data/dashboards/default.json b/data/dashboards/default.json deleted file mode 100644 index 2dc6f36ab62..00000000000 --- a/data/dashboards/default.json +++ /dev/null @@ -1,409 +0,0 @@ -{ - "title": "Grafana Play Home", - "services": { - "filter": { - "list": [], - "time": { - "from": "now-15m", - "to": "now" - } - } - }, - "rows": [ - { - "title": "test", - "height": "190px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "error": false, - "span": 12, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "html", - "content": "

Welcome to grafana demo, playground and interactive tutorial site.

\n\n
\n\t
\n\t\t

Feature showcases

\n\t\t\n\t
\n\t
\n\t\t

Graphite tutorials

\n\t\t
    \n\t\t\t
  • \n\t\t\t\tGraphite introduction (TODO)\n\t\t\t
  • \n\t\t\t
  • \n\t\t\t\tBasic functions (TODO)\n\t\t\t
  • \n\t\t\t
  • \n\t\t\t\tAdvanced functions (TODO)\n\t\t\t
  • \n\t\t\t
  • \n\t\t\t\tTips and tricks (TODO)\n\t\t\t
  • \n\t\t
\n\t
\n\t
\n\t\t

InfluxDB examples

\n\t\t
    \n\t\t\t
  • \n\t\t\t\tTODO\n\t\t\t
  • \n\t\t
\n\t
\n
\n\n", - "style": {}, - "title": "Grafana demo site" - } - ], - "notice": false - }, - { - "title": "test", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 3, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(scaleToSeconds(apps.fakesite.*.counters.requests.count,1),2)" - } - ], - "aliasColors": { - "web_server_04": "#3F6833", - "web_server_03": "#508642", - "web_server_02": "#7EB26D", - "web_server_01": "#B7DBAB" - }, - "aliasYAxis": {}, - "title": "server requests", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": true, - "max": true, - "current": true, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "alias(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),'logins')" - }, - { - "target": "alias(timeShift(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),'1h'),'logins (-1 hour)')" - } - ], - "aliasColors": { - "logins": "#7EB26D", - "logins (-1 day)": "#447EBC" - }, - "aliasYAxis": {}, - "title": "logins", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - } - ], - "notice": false - }, - { - "title": "", - "height": "300px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 4, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "bytes", - "none" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 0, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": true, - "max": false, - "current": true, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "alias(scale(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),1000000),'memory')" - }, - { - "target": "alias(scaleToSeconds(apps.fakesite.web_server_01.counters.request_status.code_302.count,1),'cpu')" - } - ], - "aliasColors": { - "cpu": "#E24D42" - }, - "aliasYAxis": { - "cpu": 2 - }, - "title": "Memory / CPU", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "span": 8, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "ms", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": false, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": true, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(statsd.fakesite.timers.ads_timer.*,4)" - } - ], - "aliasColors": { - "upper_75": "#EAB839", - "upper_50": "#7EB26D", - "upper_25": "#BA43A9" - }, - "aliasYAxis": {}, - "title": "client side full page load", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - } - ], - "notice": false - }, - { - "title": "test", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [], - "notice": false - } - ], - "editable": true, - "failover": false, - "panel_hints": true, - "style": "dark", - "pulldowns": [ - { - "type": "filtering", - "collapse": false, - "notice": false, - "enable": false - }, - { - "type": "annotations", - "enable": false - } - ], - "nav": [ - { - "type": "timepicker", - "collapse": false, - "notice": false, - "enable": true, - "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "now": true - } - ], - "loader": { - "save_gist": false, - "save_elasticsearch": true, - "save_local": true, - "save_default": true, - "save_temp": true, - "save_temp_ttl_enable": true, - "save_temp_ttl": "30d", - "load_gist": false, - "load_elasticsearch": true, - "load_elasticsearch_size": 20, - "load_local": false, - "hide": false - }, - "refresh": false, - "tags": [ - "showcase", - "startpage", - "home", - "default" - ], - "timezone": "browser" -} \ No newline at end of file diff --git a/data/dashboards/grafana-play-home.json b/data/dashboards/grafana-play-home.json deleted file mode 100644 index 49061346f2a..00000000000 --- a/data/dashboards/grafana-play-home.json +++ /dev/null @@ -1 +0,0 @@ -{"editable":true,"nav":[{"collapse":false,"enable":true,"notice":false,"now":true,"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"status":"Stable","time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"],"type":"timepicker"}],"pulldowns":[{"collapse":false,"enable":false,"notice":false,"type":"filtering"},{"enable":false,"type":"annotations"}],"rows":[{"collapsable":true,"collapse":false,"editable":true,"height":"190px","notice":false,"panels":[{"content":"\u003ch3 class=\"text-center\"\u003eWelcome to grafana demo, playground and interactive tutorial site.\u003c/h2\u003e\n\n\u003cdiv class=\"row-fluid\"\u003e\n\u0009\u003cdiv class=\"span4\"\u003e\n\u0009\u0009\u003ch4\u003eFeature showcases\u003c/h2\u003e\n\u0009\u0009\u003cul\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/graph-styles.json\"\u003eGraphs styles\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/templated-graphs.json\"\u003eTemplated graphs\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/templated-graphs-nested.json\"\u003eTemplated graphs nested\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/annotations.json\"\u003eAnnotations\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/white-theme.json\"\u003eWhite theme\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u003c/ul\u003e\n\u0009\u003c/div\u003e\n\u0009\u003cdiv class=\"span4\"\u003e\n\u0009\u0009\u003ch4\u003eGraphite tutorials\u003c/h2\u003e\n\u0009\u0009\u003cul\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009Graphite introduction (TODO)\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009Basic functions (TODO)\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009Advanced functions (TODO)\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009Tips and tricks (TODO)\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u003c/ul\u003e\n\u0009\u003c/div\u003e\n\u0009\u003cdiv class=\"span4\"\u003e\n\u0009\u0009\u003ch4\u003eInfluxDB examples\u003c/h2\u003e\n\u0009\u0009\u003cul\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009TODO\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u003c/ul\u003e\n\u0009\u003c/div\u003e\n\u003c/div\u003e\n\n\u003cscript type=\"text/javascript\"\u003e(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){\n(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),\nm=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)\n})(window,document,'script','//www.google-analytics.com/analytics.js','ga');\n\nga('create', 'UA-47280256-1', 'grafana.org');\nga('send', 'pageview');\u003c/script\u003e","editable":true,"error":false,"id":1,"loadingEditor":false,"mode":"html","span":12,"style":{},"title":"Grafana demo site","type":"text"}],"title":"test"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"web_server_01":"#B7DBAB","web_server_02":"#7EB26D","web_server_03":"#508642","web_server_04":"#3F6833"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":3,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":2,"interactive":true,"legend":{"avg":false,"current":false,"max":false,"min":false,"show":true,"total":false,"values":false},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"target":"aliasByNode(scaleToSeconds(apps.fakesite.*.counters.requests.count,1),2)"}],"timezone":"browser","title":"server requests","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["short","short"],"zerofill":true},{"aliasColors":{"logins":"#7EB26D","logins (-1 day)":"#447EBC"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":3,"interactive":true,"legend":{"avg":false,"current":true,"max":true,"min":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"alias(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),'logins')"},{"target":"alias(timeShift(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),'1h'),'logins (-1 hour)')"}],"timezone":"browser","title":"logins","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["short","short"],"zerofill":true}],"title":"test"},{"collapsable":true,"collapse":false,"editable":true,"height":"300px","notice":false,"panels":[{"aliasColors":{"cpu":"#E24D42"},"aliasYAxis":{"cpu":2},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":0,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":4,"interactive":true,"legend":{"avg":false,"current":true,"max":false,"min":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"alias(scale(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),1000000),'memory')"},{"target":"alias(scaleToSeconds(apps.fakesite.web_server_01.counters.request_status.code_302.count,1),'cpu')"}],"timezone":"browser","title":"Memory / CPU","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["bytes","none"],"zerofill":true},{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":true,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":5,"interactive":true,"legend":{"alignAsTable":true,"avg":true,"current":false,"max":false,"min":false,"rightSide":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":false,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"target":"aliasByNode(statsd.fakesite.timers.ads_timer.*,4)"}],"timezone":"browser","title":"client side full page load","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["ms","short"],"zerofill":true}],"title":""},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[],"title":"test"}],"style":"dark","tags":["showcase","startpage","home","default"],"templating":{"list":[]},"time":{"from":"now-15m","to":"now"},"timezone":"browser","title":"Grafana Play Home","version":3} \ No newline at end of file diff --git a/data/dashboards/graph-styles.json b/data/dashboards/graph-styles.json deleted file mode 100644 index 4f9147cdf62..00000000000 --- a/data/dashboards/graph-styles.json +++ /dev/null @@ -1,796 +0,0 @@ -{ - "title": "Graph styles", - "services": { - "filter": { - "list": [], - "time": { - "from": "now-15m", - "to": "now" - } - } - }, - "rows": [ - { - "title": "Simple graph", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 0, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(scaleToSeconds(apps.backend.*.counters.requests.count,1),2)" - } - ], - "aliasColors": { - "web_server_04": "#E24D42", - "web_server_03": "#508642", - "web_server_02": "#EAB839", - "web_server_01": "#EF843C" - }, - "aliasYAxis": {}, - "title": "Simple graph", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Simple graph\n- Click on the title and select edit to open edit mode\n- The display styles tab allows you change line width, fill, stacking, and more\n- You can change a series color by clicking the colored line in the legend ", - "style": {}, - "title": "Description" - } - ], - "notice": false - }, - { - "title": "Stacked Graph", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 2, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(scaleToSeconds(apps.fakesite.*.counters.requests.count,1),2)" - } - ], - "aliasColors": { - "web_server_04": "#E24D42", - "web_server_03": "#EF843C", - "web_server_02": "#EAB839", - "web_server_01": "#F2C96D" - }, - "aliasYAxis": {}, - "title": "Stacked lines", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Stacked graph\n- This graph shows stacked series, with area fill and 2px line width\n- We have also added legend values. These can be enabled in the Grid & Axes tab in edit mode. \n- Legend values can be Min, Max, Total, Current and Average", - "style": {}, - "title": "Description" - } - ], - "notice": false - }, - { - "title": "Staircase line", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "ms", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": true, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(statsd.fakesite.timers.ads_timer.upper_90,4)" - }, - { - "target": "aliasByNode(statsd.fakesite.timers.ads_timer.upper_90,4)" - } - ], - "aliasColors": { - "upper_75": "#EAB839", - "upper_50": "#7EB26D", - "upper_25": "#BA43A9" - }, - "aliasYAxis": {}, - "title": "Staircase line", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Staircase & Y-Axis format\n- In display styles tab you can switch to staircase line \n- In Axes & Grid tab you can change to different Y units & formats.\n", - "style": {}, - "title": "Description" - } - ], - "notice": false - }, - { - "title": "Right Y-Axis", - "height": "300px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "bytes", - "none" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 0, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": true, - "max": false, - "current": true, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "alias(scale(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),1000000),'memory')" - }, - { - "target": "alias(scaleToSeconds(apps.fakesite.web_server_02.counters.requests.count,1),'cpu')" - } - ], - "aliasColors": { - "cpu": "#E24D42" - }, - "aliasYAxis": { - "cpu": 2 - }, - "title": "Memory / CPU", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Second Y-Axis\n- Click on the series legend color line to open the color selector\n- In the series color selector popup you can also move the series to the Right-Y axis\n- Multiple Y-Axis are great for showing to related series that have different magnitudes ", - "style": {}, - "title": "Description" - } - ], - "notice": false - }, - { - "title": "test", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "none", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "null", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(apps.fakesite.web_server_01.counters.request_status.code_404.count,4)" - } - ], - "aliasColors": { - "upper_75": "#EAB839", - "upper_50": "#7EB26D", - "upper_25": "#BA43A9" - }, - "aliasYAxis": {}, - "title": "Null point mode", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Null point mode\n- This option under Display styles tab controls how null values are handled\n- The graph to left shows how the default \"null\" looks. \n- __null__ null values are left null and this leaves empty spaces in the graph\n- __null as zero__ null values are drawn as zero values\n- __connected__ null values are ignored and the line jumps directly to the next value.", - "style": {}, - "title": "Description" - } - ], - "notice": false - }, - { - "title": "Thresholds", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "none", - "short" - ], - "grid": { - "max": 700, - "min": 0, - "threshold1": 400, - "threshold2": 600, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 0, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": true, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(apps.fakesite.web_server_01.counters.requests.count,4)" - } - ], - "aliasColors": { - "upper_75": "#EAB839", - "upper_50": "#7EB26D", - "upper_25": "#BA43A9", - "requests": "#6ED0E0" - }, - "aliasYAxis": {}, - "title": "Thresholds", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Thresholds\n- You can define thresholds in the Grid & Axes tab in edit mode \n- You can define one or two thresholds, color is also changeable. \n- You can have lower bound thresholds as well. ", - "style": {}, - "title": "Description" - } - ], - "notice": false - }, - { - "title": "Bars", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "ms", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": false, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": true, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": true, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(statsd.fakesite.timers.ads_timer.*,4)" - } - ], - "aliasColors": { - "upper_75": "#EAB839", - "upper_50": "#7EB26D", - "upper_25": "#BA43A9" - }, - "aliasYAxis": {}, - "title": "Bars", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Bars\n- In display styles tab you can switch from line to bars\n- The width of the bar is relative to the pixel width between two values\n", - "style": {}, - "title": "Description" - } - ], - "notice": false - }, - { - "title": "Graphite PNG", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "ms", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": false, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 2, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(apps.backend.*.counters.requests.count,2)" - } - ], - "aliasColors": { - "upper_75": "#EAB839", - "upper_50": "#7EB26D", - "upper_25": "#BA43A9" - }, - "aliasYAxis": {}, - "title": "Graphite PNG", - "datasource": null, - "renderer": "png", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Graphite PNG support\n- You can switch from client side rendering to graphite's server side PNG rendering\n- You cannot click and drag to zoom in in this render mode.\n", - "style": {}, - "title": "Description" - } - ], - "notice": false - } - ], - "editable": true, - "failover": false, - "panel_hints": true, - "style": "dark", - "pulldowns": [ - { - "type": "filtering", - "collapse": false, - "notice": false, - "enable": false - }, - { - "type": "annotations", - "enable": false - } - ], - "nav": [ - { - "type": "timepicker", - "collapse": false, - "notice": false, - "enable": true, - "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "now": true - } - ], - "loader": { - "save_gist": false, - "save_elasticsearch": true, - "save_local": true, - "save_default": true, - "save_temp": true, - "save_temp_ttl_enable": true, - "save_temp_ttl": "30d", - "load_gist": false, - "load_elasticsearch": true, - "load_elasticsearch_size": 20, - "load_local": false, - "hide": false - }, - "refresh": false, - "tags": [ - "showcase", - "annotations" - ], - "timezone": "browser" -} \ No newline at end of file diff --git a/data/dashboards/graph_styles_collapsed.json b/data/dashboards/graph_styles_collapsed.json deleted file mode 100644 index 0467ac0d335..00000000000 --- a/data/dashboards/graph_styles_collapsed.json +++ /dev/null @@ -1 +0,0 @@ -{"editable":true,"nav":[{"collapse":false,"enable":true,"notice":false,"now":false,"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"status":"Stable","time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"],"type":"timepicker"}],"pulldowns":[{"collapse":false,"enable":false,"notice":false,"type":"filtering"},{"enable":false,"type":"annotations"}],"refresh":false,"rows":[{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":0,"grid":{"leftMax":null,"leftMin":null,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":17,"legend":{"avg":false,"current":false,"max":false,"min":false,"show":true,"total":false,"values":false},"lines":true,"linewidth":1,"nullPointMode":"connected","percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":12,"stack":false,"steppedLine":false,"targets":[{}],"tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["short","short"],"zerofill":true}],"title":"Simple graph"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"web_server_01":"#F2C96D","web_server_02":"#EAB839","web_server_03":"#EF843C","web_server_04":"#E24D42"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":2,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":3,"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"target":"aliasByNode(scaleToSeconds(apps.fakesite.*.counters.requests.count,1),2)"}],"timezone":"browser","title":"Stacked lines","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["short","short"],"zerofill":true},{"content":"#### Stacked graph\n- This graph shows stacked series, with area fill and 2px line width\n- We have also added legend values. These can be enabled in the Grid \u0026 Axes tab in edit mode. \n- Legend values can be Min, Max, Total, Current and Average","editable":true,"error":false,"id":4,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Stacked Graph"},{"collapsable":true,"collapse":true,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":5,"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":true,"targets":[{"target":"aliasByNode(statsd.fakesite.timers.ads_timer.upper_90,4)"},{"target":"aliasByNode(statsd.fakesite.timers.ads_timer.upper_90,4)"}],"timezone":"browser","title":"Staircase line","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["ms","short"],"zerofill":true},{"content":"#### Staircase \u0026 Y-Axis format\n- In display styles tab you can switch to staircase line \n- In Axes \u0026 Grid tab you can change to different Y units \u0026 formats.\n","editable":true,"error":false,"id":6,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Staircase line"},{"collapsable":true,"collapse":true,"editable":true,"height":"300px","notice":false,"panels":[{"aliasColors":{"cpu":"#E24D42"},"aliasYAxis":{"cpu":2},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":0,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":7,"interactive":true,"legend":{"avg":false,"current":true,"max":false,"min":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"alias(scale(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),1000000),'memory')"},{"target":"alias(scaleToSeconds(apps.fakesite.web_server_02.counters.requests.count,1),'cpu')"}],"timezone":"browser","title":"Memory / CPU","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["bytes","none"],"zerofill":true},{"content":"#### Second Y-Axis\n- Click on the series legend color line to open the color selector\n- In the series color selector popup you can also move the series to the Right-Y axis\n- Multiple Y-Axis are great for showing to related series that have different magnitudes ","editable":true,"error":false,"id":8,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Right Y-Axis"},{"collapsable":true,"collapse":true,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":9,"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"null","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"aliasByNode(apps.fakesite.web_server_01.counters.request_status.code_404.count,4)"}],"timezone":"browser","title":"Null point mode","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["none","short"],"zerofill":true},{"content":"#### Null point mode\n- This option under Display styles tab controls how null values are handled\n- The graph to left shows how the default \"null\" looks. \n- __null__ null values are left null and this leaves empty spaces in the graph\n- __null as zero__ null values are drawn as zero values\n- __connected__ null values are ignored and the line jumps directly to the next value.","editable":true,"error":false,"id":10,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"test"},{"collapsable":true,"collapse":true,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"requests":"#6ED0E0","upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":0,"grid":{"leftMax":700,"leftMin":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":400,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":600,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":11,"interactive":true,"legend":{"avg":false,"current":false,"max":false,"min":false,"show":true,"total":true,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"aliasByNode(apps.fakesite.web_server_01.counters.requests.count,4)"}],"timezone":"browser","title":"Thresholds","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["none","short"],"zerofill":true},{"content":"#### Thresholds\n- You can define thresholds in the Grid \u0026 Axes tab in edit mode \n- You can define one or two thresholds, color is also changeable. \n- You can have lower bound thresholds as well. ","editable":true,"error":false,"id":12,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Thresholds"},{"collapsable":true,"collapse":true,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":true,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":13,"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":false,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":true,"targets":[{"target":"aliasByNode(statsd.fakesite.timers.ads_timer.*,4)"}],"timezone":"browser","title":"Bars","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["ms","short"],"zerofill":true},{"content":"#### Bars\n- In display styles tab you can switch from line to bars\n- The width of the bar is relative to the pixel width between two values\n","editable":true,"error":false,"id":14,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Bars"},{"collapsable":true,"collapse":true,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":15,"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":false,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":2,"points":false,"renderer":"png","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"target":"aliasByNode(apps.backend.*.counters.requests.count,2)"}],"timezone":"browser","title":"Graphite PNG","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["ms","short"],"zerofill":true},{"content":"#### Graphite PNG support\n- You can switch from client side rendering to graphite's server side PNG rendering\n- You cannot click and drag to zoom in in this render mode.\n","editable":true,"error":false,"id":16,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Graphite PNG"}],"style":"dark","tags":["showcase","annotations"],"templating":{"list":[]},"time":{"from":"2014-08-16T13:36:29.354Z","to":"2014-08-16T13:51:59.670Z"},"timezone":"browser","title":"Graph_styles_collapsed","version":3} \ No newline at end of file diff --git a/data/dashboards/home.json b/data/dashboards/home.json deleted file mode 100644 index 62330969e74..00000000000 --- a/data/dashboards/home.json +++ /dev/null @@ -1 +0,0 @@ -{"editable":true,"nav":[{"collapse":false,"enable":true,"notice":false,"now":true,"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"status":"Stable","time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"],"type":"timepicker"}],"pulldowns":[{"collapse":false,"enable":false,"notice":false,"type":"filtering"},{"enable":false,"type":"annotations"}],"rows":[{"collapsable":true,"collapse":false,"editable":true,"height":"190px","notice":false,"panels":[{"content":"\u003ch3 class=\"text-center\"\u003eWelcome to grafana demo, playground and interactive tutorial site.\u003c/h2\u003e\n\n\u003cdiv class=\"row-fluid\"\u003e\n\u0009\u003cdiv class=\"span4\"\u003e\n\u0009\u0009\u003ch4\u003eFeature showcases\u003c/h2\u003e\n\u0009\u0009\u003cul\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/graph-styles.json\"\u003eGraphs styles\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/templated-graphs.json\"\u003eTemplated graphs\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/templated-graphs-nested.json\"\u003eTemplated graphs nested\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/annotations.json\"\u003eAnnotations\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/white-theme.json\"\u003eWhite theme\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u003c/ul\u003e\n\u0009\u003c/div\u003e\n\u0009\u003cdiv class=\"span4\"\u003e\n\u0009\u0009\u003ch4\u003eGraphite tutorials\u003c/h2\u003e\n\u0009\u0009\u003cul\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009Graphite introduction (TODO)\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009Basic functions (TODO)\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009Advanced functions (TODO)\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009Tips and tricks (TODO)\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u003c/ul\u003e\n\u0009\u003c/div\u003e\n\u0009\u003cdiv class=\"span4\"\u003e\n\u0009\u0009\u003ch4\u003eInfluxDB examples\u003c/h2\u003e\n\u0009\u0009\u003cul\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009TODO\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u003c/ul\u003e\n\u0009\u003c/div\u003e\n\u003c/div\u003e\n\n\u003cscript type=\"text/javascript\"\u003e(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){\n(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),\nm=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)\n})(window,document,'script','//www.google-analytics.com/analytics.js','ga');\n\nga('create', 'UA-47280256-1', 'grafana.org');\nga('send', 'pageview');\u003c/script\u003e","editable":true,"error":false,"id":1,"loadingEditor":false,"mode":"html","span":12,"style":{},"title":"Grafana demo site","type":"text"}],"title":"test"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"web_server_01":"#B7DBAB","web_server_02":"#7EB26D","web_server_03":"#508642","web_server_04":"#3F6833"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":3,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":2,"interactive":true,"legend":{"avg":false,"current":false,"max":false,"min":false,"show":true,"total":false,"values":false},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"target":"aliasByNode(scaleToSeconds(apps.fakesite.*.counters.requests.count,1),2)"}],"timezone":"browser","title":"SERVER REQUESTS","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["short","short"],"zerofill":true},{"aliasColors":{"logins":"#7EB26D","logins (-1 day)":"#447EBC"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":3,"interactive":true,"legend":{"avg":false,"current":true,"max":true,"min":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"alias(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),'logins')"},{"target":"alias(timeShift(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),'1h'),'logins (-1 hour)')"}],"timezone":"browser","title":"logins","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["short","short"],"zerofill":true}],"title":"test"},{"collapsable":true,"collapse":false,"editable":true,"height":"300px","notice":false,"panels":[{"aliasColors":{"cpu":"#E24D42"},"aliasYAxis":{"cpu":2},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":0,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":4,"interactive":true,"legend":{"avg":false,"current":true,"max":false,"min":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"alias(scale(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),1000000),'memory')"},{"target":"alias(scaleToSeconds(apps.fakesite.web_server_01.counters.request_status.code_302.count,1),'cpu')"}],"timezone":"browser","title":"Memory / CPU","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["bytes","none"],"zerofill":true},{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":true,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":5,"interactive":true,"legend":{"alignAsTable":true,"avg":true,"current":false,"max":false,"min":false,"rightSide":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":false,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"target":"aliasByNode(statsd.fakesite.timers.ads_timer.*,4)"}],"timezone":"browser","title":"client side full page load","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["ms","short"],"zerofill":true}],"title":""},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[],"title":"test"}],"style":"dark","tags":["showcase","startpage","home","default"],"templating":{"list":[]},"time":{"from":"now-15m","to":"now"},"timezone":"browser","title":"home","version":3} \ No newline at end of file diff --git a/data/dashboards/templated-graphs-nested.json b/data/dashboards/templated-graphs-nested.json deleted file mode 100644 index abd26cc8f17..00000000000 --- a/data/dashboards/templated-graphs-nested.json +++ /dev/null @@ -1,288 +0,0 @@ -{ - "title": "Templated Graphs Nested", - "services": { - "filter": { - "list": [ - { - "type": "filter", - "name": "app", - "query": "apps.*", - "includeAll": true, - "options": [ - { - "text": "All", - "value": "{backend,fakesite}" - }, - { - "text": "backend", - "value": "backend" - }, - { - "text": "fakesite", - "value": "fakesite" - } - ], - "current": { - "text": "backend", - "value": "backend" - } - }, - { - "type": "filter", - "name": "server", - "query": "apps.[[app]].*", - "includeAll": true, - "options": [ - { - "text": "All", - "value": "{backend_01,backend_02,backend_03,backend_04}" - }, - { - "text": "backend_01", - "value": "backend_01" - }, - { - "text": "backend_02", - "value": "backend_02" - }, - { - "text": "backend_03", - "value": "backend_03" - }, - { - "text": "backend_04", - "value": "backend_04" - } - ], - "current": { - "text": "All", - "value": "{backend_01,backend_02,backend_03,backend_04}" - } - } - ], - "time": { - "from": "now-6h", - "to": "now" - } - } - }, - "rows": [ - { - "title": "Row1", - "height": "350px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 12, - "editable": true, - "type": "graphite", - "loadingEditor": false, - "datasource": null, - "renderer": "flot", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "annotate": { - "enable": false - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "groupByNode(apps.[[app]].[[server]].counters.requests.count,2,'sum')" - } - ], - "aliasColors": { - "highres.test": "#1F78C1", - "scale(highres.test,3)": "#6ED0E0", - "mobile": "#6ED0E0", - "tablet": "#EAB839" - }, - "aliasYAxis": {}, - "title": "Traffic" - } - ], - "notice": false - }, - { - "title": "Row1", - "height": "350px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 12, - "editable": true, - "type": "graphite", - "loadingEditor": false, - "datasource": null, - "renderer": "flot", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "annotate": { - "enable": false - }, - "resolution": 100, - "lines": true, - "fill": 0, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(movingAverage(scaleToSeconds(apps.[[app]].[[server]].counters.requests.count,60),10),1,2)" - } - ], - "aliasColors": { - "highres.test": "#1F78C1", - "scale(highres.test,3)": "#6ED0E0", - "mobile": "#6ED0E0", - "tablet": "#EAB839" - }, - "aliasYAxis": {}, - "title": "Sessions / min" - } - ], - "notice": false - } - ], - "editable": true, - "failover": false, - "panel_hints": true, - "style": "dark", - "pulldowns": [ - { - "type": "filtering", - "collapse": false, - "notice": false, - "enable": true - }, - { - "type": "annotations", - "enable": false - } - ], - "nav": [ - { - "type": "timepicker", - "collapse": false, - "notice": false, - "enable": true, - "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "now": true - } - ], - "loader": { - "save_gist": false, - "save_elasticsearch": true, - "save_local": true, - "save_default": true, - "save_temp": true, - "save_temp_ttl_enable": true, - "save_temp_ttl": "30d", - "load_gist": false, - "load_elasticsearch": true, - "load_elasticsearch_size": 20, - "load_local": false, - "hide": false - }, - "refresh": false, - "tags": [ - "showcase", - "templated" - ], - "timezone": "browser" -} \ No newline at end of file diff --git a/data/dashboards/templated-graphs.json b/data/dashboards/templated-graphs.json deleted file mode 100644 index f979e4d1223..00000000000 --- a/data/dashboards/templated-graphs.json +++ /dev/null @@ -1,272 +0,0 @@ -{ - "title": "Templated Graphs", - "services": { - "filter": { - "list": [ - { - "type": "filter", - "name": "root", - "query": "statsd.fakesite.counters.session_start.*", - "includeAll": true, - "options": [ - { - "text": "All", - "value": "{desktop,mobile,tablet}" - }, - { - "text": "desktop", - "value": "desktop" - }, - { - "text": "mobile", - "value": "mobile" - }, - { - "text": "tablet", - "value": "tablet" - } - ], - "current": { - "text": "All", - "value": "{desktop,mobile,tablet}" - } - } - ], - "time": { - "from": "now-6h", - "to": "now" - } - } - }, - "rows": [ - { - "title": "Row1", - "height": "350px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 12, - "editable": true, - "type": "graphite", - "loadingEditor": false, - "datasource": null, - "renderer": "flot", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "annotate": { - "enable": false - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(summarize(statsd.fakesite.counters.session_start.[[root]].count,'1min','sum'),4)" - } - ], - "aliasColors": { - "highres.test": "#1F78C1", - "scale(highres.test,3)": "#6ED0E0", - "mobile": "#6ED0E0", - "tablet": "#EAB839" - }, - "aliasYAxis": {}, - "title": "Device sessions" - } - ], - "notice": false - }, - { - "title": "Row1", - "height": "350px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "loadingEditor": false, - "datasource": null, - "renderer": "flot", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "annotate": { - "enable": false - }, - "resolution": 100, - "lines": false, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": true, - "stack": true, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(summarize(statsd.fakesite.counters.session_start.[[root]].count,'1h','sum'),4)" - } - ], - "aliasColors": { - "highres.test": "#1F78C1", - "scale(highres.test,3)": "#6ED0E0", - "tablet": "#EAB839", - "desktop": "#7EB26D", - "mobile": "#6ED0E0" - }, - "aliasYAxis": {}, - "title": "Device sessions (1h)" - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Templated metric queries / graphs \n- In dashboard settings, in the Controls tab / Feature toggles. You can enable 'Filtering' \n- This feature when enabled will show you a bar bellow the menu.\n- In this bar you can add filters, or what should be named templated metric segments. \n- A filter is a query for a specific metric segment\n- Open any graph in this dashboard and edit mode and you can see that the [[device]] filter is used instead of a wildcard.\n- Try clicking the All link in the filter menu at the top, change device and see that all graphs change to only show values for that device. ", - "style": {}, - "title": "Description" - } - ], - "notice": false - } - ], - "editable": true, - "failover": false, - "panel_hints": true, - "style": "dark", - "pulldowns": [ - { - "type": "filtering", - "collapse": false, - "notice": false, - "enable": true - }, - { - "type": "annotations", - "enable": false - } - ], - "nav": [ - { - "type": "timepicker", - "collapse": false, - "notice": false, - "enable": true, - "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "now": true - } - ], - "loader": { - "save_gist": false, - "save_elasticsearch": true, - "save_local": true, - "save_default": true, - "save_temp": true, - "save_temp_ttl_enable": true, - "save_temp_ttl": "30d", - "load_gist": false, - "load_elasticsearch": true, - "load_elasticsearch_size": 20, - "load_local": false, - "hide": false - }, - "refresh": false, - "tags": [ - "showcase", - "templated" - ], - "timezone": "browser" -} \ No newline at end of file diff --git a/data/dashboards/testing-save.json b/data/dashboards/testing-save.json deleted file mode 100644 index fdd008c4cf7..00000000000 --- a/data/dashboards/testing-save.json +++ /dev/null @@ -1 +0,0 @@ -{"editable":true,"nav":[{"collapse":false,"enable":true,"notice":false,"now":true,"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"status":"Stable","time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"],"type":"timepicker"}],"pulldowns":[{"collapse":false,"enable":false,"notice":false,"type":"filtering"},{"enable":false,"type":"annotations"}],"refresh":false,"rows":[{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"web_server_01":"#EF843C","web_server_02":"#EAB839","web_server_03":"#508642","web_server_04":"#E24D42"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":0,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"interactive":true,"legend":{"avg":false,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":1,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"aliasByNode(scaleToSeconds(apps.backend.*.counters.requests.count,1),2)"}],"timezone":"browser","title":"Simple graph","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["short","short"],"zerofill":true},{"content":"#### Simple graph\n- Click on the title and select edit to open edit mode\n- The display styles tab allows you change line width, fill, stacking, and more\n- You can change a series color by clicking the colored line in the legend ","editable":true,"error":false,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Simple graph"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"web_server_01":"#F2C96D","web_server_02":"#EAB839","web_server_03":"#EF843C","web_server_04":"#E24D42"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":2,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"target":"aliasByNode(scaleToSeconds(apps.fakesite.*.counters.requests.count,1),2)"}],"timezone":"browser","title":"Stacked lines","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["short","short"],"zerofill":true},{"content":"#### Stacked graph\n- This graph shows stacked series, with area fill and 2px line width\n- We have also added legend values. These can be enabled in the Grid \u0026 Axes tab in edit mode. \n- Legend values can be Min, Max, Total, Current and Average","editable":true,"error":false,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Stacked Graph"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":true,"targets":[{"target":"aliasByNode(statsd.fakesite.timers.ads_timer.upper_90,4)"},{"target":"aliasByNode(statsd.fakesite.timers.ads_timer.upper_90,4)"}],"timezone":"browser","title":"Staircase line","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["ms","short"],"zerofill":true},{"content":"#### Staircase \u0026 Y-Axis format\n- In display styles tab you can switch to staircase line \n- In Axes \u0026 Grid tab you can change to different Y units \u0026 formats.\n","editable":true,"error":false,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Staircase line"},{"collapsable":true,"collapse":false,"editable":true,"height":"300px","notice":false,"panels":[{"aliasColors":{"cpu":"#E24D42"},"aliasYAxis":{"cpu":2},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":0,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"interactive":true,"legend":{"avg":false,"current":true,"max":false,"min":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"alias(scale(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),1000000),'memory')"},{"target":"alias(scaleToSeconds(apps.fakesite.web_server_02.counters.requests.count,1),'cpu')"}],"timezone":"browser","title":"Memory / CPU","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["bytes","none"],"zerofill":true},{"content":"#### Second Y-Axis\n- Click on the series legend color line to open the color selector\n- In the series color selector popup you can also move the series to the Right-Y axis\n- Multiple Y-Axis are great for showing to related series that have different magnitudes ","editable":true,"error":false,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Right Y-Axis"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"null","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"aliasByNode(apps.fakesite.web_server_01.counters.request_status.code_404.count,4)"}],"timezone":"browser","title":"Null point mode","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["none","short"],"zerofill":true},{"content":"#### Null point mode\n- This option under Display styles tab controls how null values are handled\n- The graph to left shows how the default \"null\" looks. \n- __null__ null values are left null and this leaves empty spaces in the graph\n- __null as zero__ null values are drawn as zero values\n- __connected__ null values are ignored and the line jumps directly to the next value.","editable":true,"error":false,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"test"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"requests":"#6ED0E0","upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":0,"grid":{"leftMax":700,"leftMin":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":400,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":600,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"interactive":true,"legend":{"avg":false,"current":false,"max":false,"min":false,"show":true,"total":true,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"aliasByNode(apps.fakesite.web_server_01.counters.requests.count,4)"}],"timezone":"browser","title":"Thresholds","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["none","short"],"zerofill":true},{"content":"#### Thresholds\n- You can define thresholds in the Grid \u0026 Axes tab in edit mode \n- You can define one or two thresholds, color is also changeable. \n- You can have lower bound thresholds as well. ","editable":true,"error":false,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Thresholds"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":true,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":false,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":true,"targets":[{"target":"aliasByNode(statsd.fakesite.timers.ads_timer.*,4)"}],"timezone":"browser","title":"Bars","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["ms","short"],"zerofill":true},{"content":"#### Bars\n- In display styles tab you can switch from line to bars\n- The width of the bar is relative to the pixel width between two values\n","editable":true,"error":false,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Bars"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":false,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":2,"points":false,"renderer":"png","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"target":"aliasByNode(apps.backend.*.counters.requests.count,2)"}],"timezone":"browser","title":"Graphite PNG","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["ms","short"],"zerofill":true},{"content":"#### Graphite PNG support\n- You can switch from client side rendering to graphite's server side PNG rendering\n- You cannot click and drag to zoom in in this render mode.\n","editable":true,"error":false,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Graphite PNG"}],"style":"dark","tags":["showcase","annotations"],"templating":{"list":[]},"time":{"from":"now-15m","to":"now"},"timezone":"browser","title":"Testing save","version":2} \ No newline at end of file diff --git a/data/dashboards/white-theme.json b/data/dashboards/white-theme.json deleted file mode 100644 index 4ffa2fc1a60..00000000000 --- a/data/dashboards/white-theme.json +++ /dev/null @@ -1,519 +0,0 @@ -{ - "title": "White theme", - "services": { - "filter": { - "list": [], - "time": { - "from": "now-1h", - "to": "now" - } - } - }, - "rows": [ - { - "title": "test", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 3, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(scaleToSeconds(apps.fakesite.*.counters.requests.count,1),2)" - } - ], - "aliasColors": { - "web_server_04": "#3F6833", - "web_server_03": "#508642", - "web_server_02": "#7EB26D", - "web_server_01": "#B7DBAB" - }, - "aliasYAxis": {}, - "title": "server requests", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(movingAverage(scaleToSeconds(apps.fakesite.*.counters.request_status.code_302.count,1),4),2)" - } - ], - "aliasColors": { - "logins": "#7EB26D", - "logins (-1 day)": "#447EBC" - }, - "aliasYAxis": {}, - "title": "logins", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - } - ], - "notice": false - }, - { - "title": "", - "height": "300px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 4, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "bytes", - "none" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 0, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": true, - "max": false, - "current": true, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "alias(scale(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),1000000),'memory')" - }, - { - "target": "alias(scaleToSeconds(apps.fakesite.web_server_02.counters.requests.count,1),'cpu')" - } - ], - "aliasColors": { - "cpu": "#E24D42" - }, - "aliasYAxis": { - "cpu": 2 - }, - "title": "Memory / CPU", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "span": 8, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "ms", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": false, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": true, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(statsd.fakesite.timers.ads_timer.*,4)" - } - ], - "aliasColors": { - "upper_75": "#EAB839", - "upper_50": "#7EB26D", - "upper_25": "#BA43A9" - }, - "aliasYAxis": {}, - "title": "client side full page load", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - } - ], - "notice": false - }, - { - "title": "test", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(movingAverage(scaleToSeconds(apps.fakesite.*.counters.request_status.code_302.count,1),4),2)" - } - ], - "aliasColors": { - "logins": "#7EB26D", - "logins (-1 day)": "#447EBC", - "web_server_03": "#1F78C1", - "web_server_02": "#6ED0E0", - "web_server_01": "#64B0C8" - }, - "aliasYAxis": {}, - "title": "logins", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(movingAverage(scaleToSeconds(apps.fakesite.*.counters.request_status.code_302.count,1),4),2)" - } - ], - "aliasColors": { - "logins": "#7EB26D", - "logins (-1 day)": "#447EBC", - "web_server_03": "#E24D42", - "web_server_02": "#EF843C", - "web_server_01": "#EAB839" - }, - "aliasYAxis": {}, - "title": "logins", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - } - ], - "notice": false - } - ], - "editable": true, - "failover": false, - "panel_hints": true, - "style": "light", - "pulldowns": [ - { - "type": "filtering", - "collapse": false, - "notice": false, - "enable": false - }, - { - "type": "annotations", - "enable": false - } - ], - "nav": [ - { - "type": "timepicker", - "collapse": false, - "notice": false, - "enable": true, - "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "now": true - } - ], - "loader": { - "save_gist": false, - "save_elasticsearch": true, - "save_local": true, - "save_default": true, - "save_temp": true, - "save_temp_ttl_enable": true, - "save_temp_ttl": "30d", - "load_gist": false, - "load_elasticsearch": true, - "load_elasticsearch_size": 20, - "load_local": false, - "hide": false - }, - "refresh": false, - "tags": [], - "timezone": "browser" -} \ No newline at end of file diff --git a/data/png/77dedee4f4d479cea6f9d608353ed098.png b/data/png/77dedee4f4d479cea6f9d608353ed098.png deleted file mode 100644 index 43598440c814089b328d9e505ce6b2971d6f49e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64800 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 BuEqcW diff --git a/data/png/9e956287bee2bc4eabd91f9bb06ab458.png b/data/png/9e956287bee2bc4eabd91f9bb06ab458.png deleted file mode 100644 index e43d8dc1906439c90a17bd7bc8d7414f16105d15..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 73511 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�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 Date: Wed, 11 Feb 2015 12:06:33 -0700 Subject: [PATCH 018/118] Add grafana binary to .gitignore Running "go build" defaults to building a binary named grafana in the root dir. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e0a0048b514..88bb7c6b8e0 100644 --- a/.gitignore +++ b/.gitignore @@ -17,5 +17,6 @@ src/css/*.min.css /data/* /bin/* /grafana-pro +/grafana grafana.custom.ini From a6df991b76c2f3f341e9e4ef3a2c7ae0597c29ec Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Wed, 11 Feb 2015 12:12:17 -0700 Subject: [PATCH 019/118] Update README.md w/ known working build tool versions --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a99d15676c3..e5eef25d513 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,8 @@ godep restore (will pull down all golang lib dependecies in your current GOPATH) go build -o ./bin/grafana . ``` -To build less to css for frontend: +To build less to css for the frontend you will need a recent version of of node (v0.12.0), +npm (v2.5.0) and grunt (v0.4.5). Run the following: ``` npm install From 8230279932df05c5f1345093c707cbda604b9ee8 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Wed, 11 Feb 2015 15:09:28 -0700 Subject: [PATCH 020/118] Assign new ID to when importing dashboard via frontend Fixes a panic: interface conversion: interface is string, not float64 when importing a dashboard that has a non-float ID. --- src/app/directives/dashUpload.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/directives/dashUpload.js b/src/app/directives/dashUpload.js index 889438dffc9..d9f8e3e4ae9 100644 --- a/src/app/directives/dashUpload.js +++ b/src/app/directives/dashUpload.js @@ -24,6 +24,7 @@ function (angular, kbn) { return; } var title = kbn.slugifyForUrl(window.grafanaImportDashboard.title); + window.grafanaImportDashboard.id = null; $location.path('/dashboard/import/' + title); }); }; From 793eda764080572bcd1d3e48f4c92d4b38cb2177 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Wed, 11 Feb 2015 15:17:40 -0700 Subject: [PATCH 021/118] Assign new dashboard ID when importing dashboard via command-line --- pkg/cmd/import.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/import.go b/pkg/cmd/import.go index 1c3545904a2..aeea8cb17ed 100644 --- a/pkg/cmd/import.go +++ b/pkg/cmd/import.go @@ -106,6 +106,7 @@ func importDashboard(path string, accountId int64) error { if err := jsonParser.Decode(&dash.Data); err != nil { return err } + dash.Data["id"] = nil cmd := m.SaveDashboardCommand{ AccountId: accountId, From 0140a00884ace6a0905eff6531d8626a6fad8700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 12 Feb 2015 10:32:22 +0100 Subject: [PATCH 022/118] Added two columns to user table, email_verified and theme, no used right now but will probably shortly --- pkg/models/user.go | 20 +++++++++++--------- pkg/services/sqlstore/migrations.go | 9 +++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/pkg/models/user.go b/pkg/models/user.go index 10ec03791a3..81ed0123c1e 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -11,15 +11,17 @@ var ( ) type User struct { - Id int64 - Version int - Email string - Name string - Login string - Password string - Salt string - Rands string - Company string + Id int64 + Version int + Email string + Name string + Login string + Password string + Salt string + Rands string + Company string + EmailVerified bool + Theme string IsAdmin bool AccountId int64 diff --git a/pkg/services/sqlstore/migrations.go b/pkg/services/sqlstore/migrations.go index 3f7530d3c70..3c9c27a0918 100644 --- a/pkg/services/sqlstore/migrations.go +++ b/pkg/services/sqlstore/migrations.go @@ -47,6 +47,15 @@ func addUserMigrations(mg *Migrator) { &Column{Name: "updated", Type: DB_DateTime, Nullable: false}, )) + mg.AddMigration("Add email_verified flag", new(AddColumnMigration). + Table("user").Column(&Column{Name: "email_verified", Type: DB_Bool, Nullable: true})) + + mg.AddMigration("Add email_verified flag", new(AddColumnMigration). + Table("user").Column(&Column{Name: "email_verified", Type: DB_Bool, Nullable: true})) + + mg.AddMigration("Add user.theme column", new(AddColumnMigration). + Table("user").Column(&Column{Name: "theme", Type: DB_Varchar, Nullable: true, Length: 20})) + //------- user table indexes ------------------ mg.AddMigration("add unique index user.login", new(AddIndexMigration). Table("user").Columns("login").Unique()) From 2c16b0f0f3f392036d1e1f5246b0f18bbaa3a268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 12 Feb 2015 11:55:55 +0100 Subject: [PATCH 023/118] added unit test for loading configuration file --- pkg/services/sqlstore/migrations.go | 3 --- pkg/setting/setting.go | 13 +++++++------ pkg/setting/setting_test.go | 22 ++++++++++++++++++++++ 3 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 pkg/setting/setting_test.go diff --git a/pkg/services/sqlstore/migrations.go b/pkg/services/sqlstore/migrations.go index 3c9c27a0918..0b7a540015b 100644 --- a/pkg/services/sqlstore/migrations.go +++ b/pkg/services/sqlstore/migrations.go @@ -47,9 +47,6 @@ func addUserMigrations(mg *Migrator) { &Column{Name: "updated", Type: DB_DateTime, Nullable: false}, )) - mg.AddMigration("Add email_verified flag", new(AddColumnMigration). - Table("user").Column(&Column{Name: "email_verified", Type: DB_Bool, Nullable: true})) - mg.AddMigration("Add email_verified flag", new(AddColumnMigration). Table("user").Column(&Column{Name: "email_verified", Type: DB_Bool, Nullable: true})) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index a2db1a6fec8..9cb571c479d 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -98,15 +98,10 @@ var ( func init() { IsWindows = runtime.GOOS == "windows" log.NewLogger(0, "console", `{"level": 0}`) -} - -func getWorkDir() string { - p, _ := filepath.Abs(".") - return p + WorkDir, _ = filepath.Abs(".") } func findConfigFiles() []string { - WorkDir = getWorkDir() ConfRootPath = path.Join(WorkDir, "conf") filenames := make([]string, 0) @@ -152,6 +147,10 @@ func ToAbsUrl(relativeUrl string) string { return AppUrl + relativeUrl } +func loadEnvVariableOverrides() { + +} + func NewConfigContext() { configFiles := findConfigFiles() @@ -170,6 +169,8 @@ func NewConfigContext() { } } + loadEnvVariableOverrides() + AppName = Cfg.Section("").Key("app_name").MustString("Grafana") Env = Cfg.Section("").Key("app_mode").MustString("development") diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go new file mode 100644 index 00000000000..22ffbc17edb --- /dev/null +++ b/pkg/setting/setting_test.go @@ -0,0 +1,22 @@ +package setting + +import ( + "path/filepath" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestLoadingSettings(t *testing.T) { + + WorkDir, _ = filepath.Abs("../../") + + Convey("Testing loading settings from ini file", t, func() { + + Convey("Given the default ini files", func() { + NewConfigContext() + + So(AppName, ShouldEqual, "Grafana") + }) + }) +} From 79f798f67b18b5bd711e792eacedd597dd6725b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 12 Feb 2015 13:31:41 +0100 Subject: [PATCH 024/118] Configuration file options can now be overriden using environment variables using GF__ syntax, if Section name contains dots in config they are replaced with underscores, and the section name and keyname needs to be all upper case, #1473 --- pkg/services/sqlstore/migrator/migrator.go | 10 +++++----- pkg/setting/setting.go | 13 +++++++++++++ pkg/setting/setting_test.go | 10 ++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index 53e61f3d1c9..273709d7d17 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -5,9 +5,9 @@ import ( _ "github.com/go-sql-driver/mysql" "github.com/go-xorm/xorm" + "github.com/grafana/grafana/pkg/log" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" - "github.com/grafana/grafana/pkg/log" ) type Migrator struct { @@ -70,7 +70,7 @@ func (mg *Migrator) GetMigrationLog() (map[string]MigrationLog, error) { func (mg *Migrator) Start() error { if mg.LogLevel <= log.INFO { - log.Info("Migrator:: Starting DB migration") + log.Info("Migrator: Starting DB migration") } logMap, err := mg.GetMigrationLog() @@ -82,7 +82,7 @@ func (mg *Migrator) Start() error { _, exists := logMap[m.Id()] if exists { if mg.LogLevel <= log.DEBUG { - log.Debug("Migrator:: Skipping migration: %v, Already executed", m.Id()) + log.Debug("Migrator: Skipping migration: %v, Already executed", m.Id()) } continue } @@ -114,13 +114,13 @@ func (mg *Migrator) Start() error { func (mg *Migrator) exec(m Migration) error { if mg.LogLevel <= log.INFO { - log.Info("Migrator::exec migration id: %v", m.Id()) + log.Info("Migrator: exec migration id: %v", m.Id()) } err := mg.inTransaction(func(sess *xorm.Session) error { _, err := sess.Exec(m.Sql(mg.dialect)) if err != nil { - log.Error(3, "Migrator::exec FAILED migration id: %v, err: %v", m.Id(), err) + log.Error(3, "Migrator: exec FAILED migration id: %v, err: %v", m.Id(), err) return err } return nil diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 9cb571c479d..4d3ef9161c3 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -4,6 +4,7 @@ package setting import ( + "fmt" "net/url" "os" "path" @@ -148,7 +149,19 @@ func ToAbsUrl(relativeUrl string) string { } func loadEnvVariableOverrides() { + for _, section := range Cfg.Sections() { + for _, key := range section.Keys() { + sectionName := strings.ToUpper(strings.Replace(section.Name(), ".", "_", -1)) + keyName := strings.ToUpper(strings.Replace(key.Name(), ".", "_", -1)) + envKey := fmt.Sprintf("GF_%s_%s", sectionName, keyName) + envValue := os.Getenv(envKey) + if len(envValue) > 0 { + log.Info("Setting: ENV override found: %s", envKey) + key.SetValue(envValue) + } + } + } } func NewConfigContext() { diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index 22ffbc17edb..be031e3f4cc 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -1,6 +1,7 @@ package setting import ( + "os" "path/filepath" "testing" @@ -17,6 +18,15 @@ func TestLoadingSettings(t *testing.T) { NewConfigContext() So(AppName, ShouldEqual, "Grafana") + So(AdminUser, ShouldEqual, "admin") }) + + Convey("Should be able to override via environment variables", func() { + os.Setenv("GF_SECURITY_ADMIN_USER", "superduper") + NewConfigContext() + + So(AdminUser, ShouldEqual, "superduper") + }) + }) } From 366a9f71ad5b6cf85095c2bc195d9c75b42ddba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 12 Feb 2015 13:44:22 +0100 Subject: [PATCH 025/118] Updated readme build instructions --- README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index e5eef25d513..192a1f582e1 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,8 @@ Building cd $GOPATH/src/github.com/grafana/grafana git checkout -t origin/develop go run build.go setup (only needed once to install godep) -go run build.go build -``` - -For quicker builds: - -``` godep restore (will pull down all golang lib dependecies in your current GOPATH) -go build -o ./bin/grafana . +go build . ``` To build less to css for the frontend you will need a recent version of of node (v0.12.0), From 29607d895157bf82d898cf8e69c1108bf4ba57c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 12 Feb 2015 13:45:53 +0100 Subject: [PATCH 026/118] Updated readme again --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 192a1f582e1..f3a399597a5 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ Building ``` cd $GOPATH/src/github.com/grafana/grafana git checkout -t origin/develop -go run build.go setup (only needed once to install godep) -godep restore (will pull down all golang lib dependecies in your current GOPATH) +go run build.go setup (only needed once to install godep) +godep restore (will pull down all golang lib dependecies in your current GOPATH) go build . ``` From e65a6cc063fa4e1cb549ee91b39e38f07cb3d896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 12 Feb 2015 15:46:14 +0100 Subject: [PATCH 027/118] added an inital admin settings view, very basic right now only displays all config options in grafana.ini --- pkg/api/admin_settings.go | 28 +++++++++++++++ pkg/api/api.go | 2 ++ src/app/controllers/sidemenuCtrl.js | 5 --- src/app/features/account/all.js | 7 ++++ src/app/features/admin/adminCtrl.js | 18 ---------- src/app/features/admin/adminSettingsCtrl.js | 25 ++++++++++++++ src/app/features/admin/all.js | 5 +++ src/app/features/admin/partials/admin.html | 10 ------ .../features/admin/partials/edit_user.html | 2 +- src/app/features/admin/partials/settings.html | 34 +++++++++++++++++++ src/app/features/admin/partials/users.html | 2 +- src/app/features/all.js | 10 ++---- src/app/routes/backend/all.js | 6 ++-- src/css/less/admin.less | 10 ++++++ src/css/less/grafana.less | 1 + 15 files changed, 119 insertions(+), 46 deletions(-) create mode 100644 pkg/api/admin_settings.go create mode 100644 src/app/features/account/all.js delete mode 100644 src/app/features/admin/adminCtrl.js create mode 100644 src/app/features/admin/adminSettingsCtrl.js create mode 100644 src/app/features/admin/all.js delete mode 100644 src/app/features/admin/partials/admin.html create mode 100644 src/app/features/admin/partials/settings.html create mode 100644 src/css/less/admin.less diff --git a/pkg/api/admin_settings.go b/pkg/api/admin_settings.go new file mode 100644 index 00000000000..c126c1108c6 --- /dev/null +++ b/pkg/api/admin_settings.go @@ -0,0 +1,28 @@ +package api + +import ( + "strings" + + "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/setting" +) + +func AdminGetSettings(c *middleware.Context) { + settings := make(map[string]interface{}) + + for _, section := range setting.Cfg.Sections() { + jsonSec := make(map[string]interface{}) + settings[section.Name()] = jsonSec + + for _, key := range section.Keys() { + keyName := key.Name() + value := key.Value() + if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") { + value = "************" + } + jsonSec[keyName] = value + } + } + + c.JSON(200, settings) +} diff --git a/pkg/api/api.go b/pkg/api/api.go index a9f6b5d5dc6..28e2ea8ab3d 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -30,6 +30,7 @@ func Register(r *macaron.Macaron) { r.Get("/account/users/", reqSignedIn, Index) r.Get("/account/apikeys/", reqSignedIn, Index) r.Get("/account/import/", reqSignedIn, Index) + r.Get("/admin/settings", reqGrafanaAdmin, Index) r.Get("/admin/users", reqGrafanaAdmin, Index) r.Get("/admin/users/create", reqGrafanaAdmin, Index) r.Get("/admin/users/edit/:id", reqGrafanaAdmin, Index) @@ -94,6 +95,7 @@ func Register(r *macaron.Macaron) { // admin api r.Group("/api/admin", func() { + r.Get("/settings", AdminGetSettings) r.Get("/users", AdminSearchUsers) r.Get("/users/:id", AdminGetUser) r.Post("/users", bind(dtos.AdminCreateUserForm{}), AdminCreateUser) diff --git a/src/app/controllers/sidemenuCtrl.js b/src/app/controllers/sidemenuCtrl.js index cb5027d3dd2..211ca4451c0 100644 --- a/src/app/controllers/sidemenuCtrl.js +++ b/src/app/controllers/sidemenuCtrl.js @@ -40,11 +40,6 @@ function (angular, _, $, config) { text: "Admin", href: $scope.getUrl("/admin/users"), icon: "fa fa-cube", requireSignedIn: true, - links: [ - { text: 'Settings', href: $scope.getUrl("/admin/settings")}, - { text: 'Users', href: $scope.getUrl("/admin/users"), icon: "fa fa-lock" }, - { text: 'Log', href: "", icon: "fa fa-lock" }, - ] }); } diff --git a/src/app/features/account/all.js b/src/app/features/account/all.js new file mode 100644 index 00000000000..7c01d7b8c1c --- /dev/null +++ b/src/app/features/account/all.js @@ -0,0 +1,7 @@ +define([ + './accountUsersCtrl', + './datasourcesCtrl', + './apiKeysCtrl', + './importCtrl', + './accountCtrl', +], function () {}); diff --git a/src/app/features/admin/adminCtrl.js b/src/app/features/admin/adminCtrl.js deleted file mode 100644 index bda82618503..00000000000 --- a/src/app/features/admin/adminCtrl.js +++ /dev/null @@ -1,18 +0,0 @@ -define([ - 'angular', -], -function (angular) { - 'use strict'; - - var module = angular.module('grafana.controllers'); - - module.controller('AdminCtrl', function($scope) { - - $scope.init = function() { - $scope.editor = {index: 0}; - }; - - $scope.init(); - - }); -}); diff --git a/src/app/features/admin/adminSettingsCtrl.js b/src/app/features/admin/adminSettingsCtrl.js new file mode 100644 index 00000000000..e04372bd6ac --- /dev/null +++ b/src/app/features/admin/adminSettingsCtrl.js @@ -0,0 +1,25 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('AdminSettingsCtrl', function($scope, backendSrv) { + + $scope.init = function() { + $scope.accounts = []; + $scope.getUsers(); + }; + + $scope.getUsers = function() { + backendSrv.get('/api/admin/settings').then(function(settings) { + $scope.settings = settings; + }); + }; + + $scope.init(); + + }); +}); diff --git a/src/app/features/admin/all.js b/src/app/features/admin/all.js new file mode 100644 index 00000000000..35f1f4f5db9 --- /dev/null +++ b/src/app/features/admin/all.js @@ -0,0 +1,5 @@ +define([ + './adminUsersCtrl', + './adminEditUserCtrl', + './adminSettingsCtrl', +], function () {}); diff --git a/src/app/features/admin/partials/admin.html b/src/app/features/admin/partials/admin.html deleted file mode 100644 index 1198f8b5777..00000000000 --- a/src/app/features/admin/partials/admin.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - -
-
diff --git a/src/app/features/admin/partials/edit_user.html b/src/app/features/admin/partials/edit_user.html index e9db377df5a..ecc03fd1aad 100644 --- a/src/app/features/admin/partials/edit_user.html +++ b/src/app/features/admin/partials/edit_user.html @@ -1,6 +1,6 @@
IdName Login EmailName Admin
{{user.id}}{{user.name}} {{user.login}} {{user.email}}{{user.name}} {{user.isAdmin}} - + Edit diff --git a/src/app/features/all.js b/src/app/features/all.js index a262a57b5ba..6ebabafcbad 100644 --- a/src/app/features/all.js +++ b/src/app/features/all.js @@ -16,5 +16,6 @@ define([ './account/accountCtrl', './admin/adminUsersCtrl', './admin/adminCtrl', + './admin/adminEditUserCtrl', './grafanaDatasource/datasource', ], function () {}); diff --git a/src/app/features/profile/partials/profile.html b/src/app/features/profile/partials/profile.html index 2a839db6e10..db9d9b52702 100644 --- a/src/app/features/profile/partials/profile.html +++ b/src/app/features/profile/partials/profile.html @@ -27,7 +27,7 @@ Email
  • - +
  • diff --git a/src/app/routes/backend/all.js b/src/app/routes/backend/all.js index 2ffd773ea82..e9d5d8ac95d 100644 --- a/src/app/routes/backend/all.js +++ b/src/app/routes/backend/all.js @@ -62,6 +62,14 @@ define([ templateUrl: 'app/features/admin/partials/users.html', controller : 'AdminUsersCtrl', }) + .when('/admin/users/create', { + templateUrl: 'app/features/admin/partials/edit_user.html', + controller : 'AdminEditUserCtrl', + }) + .when('/admin/users/edit/:id', { + templateUrl: 'app/features/admin/partials/edit_user.html', + controller : 'AdminEditUserCtrl', + }) .when('/login', { templateUrl: 'app/partials/login.html', controller : 'LoginCtrl', From add4adeec9cf5cb78123379c5ae5a102eecf7130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 10 Feb 2015 16:26:23 +0100 Subject: [PATCH 011/118] Fixed links to user admin views, and backend html routes for user admin edit/create urls, Closes #1451 --- pkg/api/api.go | 2 ++ src/app/features/admin/partials/edit_user.html | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 4595869ab0b..4da20db956f 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -31,6 +31,8 @@ func Register(r *macaron.Macaron) { r.Get("/account/apikeys/", reqSignedIn, Index) r.Get("/account/import/", reqSignedIn, Index) r.Get("/admin/users", reqGrafanaAdmin, Index) + r.Get("/admin/users/create", reqGrafanaAdmin, Index) + r.Get("/admin/users/edit/:id", reqGrafanaAdmin, Index) r.Get("/dashboard/*", reqSignedIn, Index) // sign up diff --git a/src/app/features/admin/partials/edit_user.html b/src/app/features/admin/partials/edit_user.html index 4209b280d5b..e9db377df5a 100644 --- a/src/app/features/admin/partials/edit_user.html +++ b/src/app/features/admin/partials/edit_user.html @@ -2,8 +2,8 @@ From 8d130421c4a4910da718ce9a74c991d827dae9f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 10 Feb 2015 17:10:07 +0100 Subject: [PATCH 012/118] Updated readme with default admin user info --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0e5ae0c7f9c..a99d15676c3 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ issues to fix and polish missing. But feedback on what is done and bug reports w ``` docker run -i -p 3000:3000 grafana/grafana:develop ``` +The default admin user is admin/admin. ## building and running From 39a6bf978431a9560bc4bb8659cf1df45bfa4c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 10 Feb 2015 19:38:55 +0100 Subject: [PATCH 013/118] Updated readme with info about the Grafana 2.0 develop branch --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 98f47aa6357..dc9ed582fda 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,12 @@ Graphite, InfluxDB & OpenTSDB. ![](http://grafana.org/assets/img/start_page_bg.png) +## Grafana 2.0 News +Grafana 2.0 with its new awesome backend written in Go is approaching beta. +The code is available in the [develop](https://github.com/grafana/grafana/tree/develop) branch. +[This blog post](http://grafana.org/blog/2015/02/10/Grafana-2-Alpha-and-preview.html) contains some news about it as well. + + ## Features ### Graphite Target Editor - Graphite target expression parser From 441e2ad2f30dc20f28c47f1c53ca7c3134240dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 11 Feb 2015 13:53:42 +0100 Subject: [PATCH 014/118] Updated dockerfile and moved to docker/production folder --- Dockerfile => docker/production/Dockerfile | 1 + docker/production/build.sh | 15 +++++++++++++++ 2 files changed, 16 insertions(+) rename Dockerfile => docker/production/Dockerfile (88%) create mode 100755 docker/production/build.sh diff --git a/Dockerfile b/docker/production/Dockerfile similarity index 88% rename from Dockerfile rename to docker/production/Dockerfile index c1a2d3cfc62..19db258d32a 100644 --- a/Dockerfile +++ b/docker/production/Dockerfile @@ -10,6 +10,7 @@ ADD tmp/ /opt/grafana/ EXPOSE 3000 VOLUME ["/opt/grafana/data"] +VOLUME ["/opt/grafana/conf"] WORKDIR /opt/grafana/ ENTRYPOINT ["./grafana", "web"] diff --git a/docker/production/build.sh b/docker/production/build.sh new file mode 100755 index 00000000000..4179d8e032e --- /dev/null +++ b/docker/production/build.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +cp Dockerfile ../../ +cd ../../ + +go run build.go build + +npm grunt release + +docker build --tag "grafana/grafana:develop" . + +rm Dockerfile +cd docker/production + + From 17f1224a5d4b78ac5cd197f526ccf2fc7d00fced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 11 Feb 2015 15:32:19 +0100 Subject: [PATCH 015/118] Fixed light theme condition, Fixes #1462 --- benchmarks/ab/ab_test.sh | 4 ++++ src/views/index.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100755 benchmarks/ab/ab_test.sh diff --git a/benchmarks/ab/ab_test.sh b/benchmarks/ab/ab_test.sh new file mode 100755 index 00000000000..c52d24a179d --- /dev/null +++ b/benchmarks/ab/ab_test.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +ab -n 20000 -c 100 -H "Authorization: Bearer vEustw23NSOZ27y3zlj28ZL3B7BpBk5kqR85DOfT5AwiS3nCi33dnsk6nhvXhZdn" \ + http://localhost:3000/api/dashboards/db/dash1 diff --git a/src/views/index.html b/src/views/index.html index 328b5113ef7..d386f825d6d 100644 --- a/src/views/index.html +++ b/src/views/index.html @@ -25,7 +25,7 @@ - +
    From 71f09ddd94987d094840069b4b7c21c925c1582b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 11 Feb 2015 16:47:22 +0100 Subject: [PATCH 016/118] Added delete user action to user admin api, and made it work in UI, Closes #1466, #1446 --- pkg/api/admin_users.go | 16 +++++++++++++--- pkg/api/api.go | 1 + pkg/models/user.go | 4 ++++ pkg/services/sqlstore/user.go | 10 ++++++++++ src/app/features/admin/adminUsersCtrl.js | 4 +++- src/app/features/admin/partials/users.html | 2 +- 6 files changed, 32 insertions(+), 5 deletions(-) diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index 0ffe0612ac0..9402eb41b20 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -8,9 +8,6 @@ import ( ) func AdminSearchUsers(c *middleware.Context) { - // query := c.QueryStrings("q") - // page := c.QueryStrings("p") - query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 20} if err := bus.Dispatch(&query); err != nil { c.JsonApiErr(500, "Failed to fetch users", err) @@ -93,3 +90,16 @@ func AdminUpdateUser(c *middleware.Context, form dtos.AdminUpdateUserForm) { c.JsonOK("User updated") } + +func AdminDeleteUser(c *middleware.Context) { + userId := c.ParamsInt64(":id") + + cmd := m.DeleteUserCommand{UserId: userId} + + if err := bus.Dispatch(&cmd); err != nil { + c.JsonApiErr(500, "Failed to delete user", err) + return + } + + c.JsonOK("User deleted") +} diff --git a/pkg/api/api.go b/pkg/api/api.go index 4da20db956f..a9f6b5d5dc6 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -98,6 +98,7 @@ func Register(r *macaron.Macaron) { r.Get("/users/:id", AdminGetUser) r.Post("/users", bind(dtos.AdminCreateUserForm{}), AdminCreateUser) r.Put("/users/:id", bind(dtos.AdminUpdateUserForm{}), AdminUpdateUser) + r.Delete("/users/:id", AdminDeleteUser) }, reqGrafanaAdmin) // rendering diff --git a/pkg/models/user.go b/pkg/models/user.go index b63ded299c7..10ec03791a3 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -50,6 +50,10 @@ type UpdateUserCommand struct { UserId int64 `json:"-"` } +type DeleteUserCommand struct { + UserId int64 +} + type SetUsingAccountCommand struct { UserId int64 AccountId int64 diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index ac7286af77c..2034b0ff347 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -1,6 +1,7 @@ package sqlstore import ( + "fmt" "strings" "time" @@ -23,6 +24,7 @@ func init() { bus.AddHandler("sql", GetSignedInUser) bus.AddHandler("sql", SearchUsers) bus.AddHandler("sql", GetUserAccounts) + bus.AddHandler("sql", DeleteUser) } func getAccountIdForNewUser(userEmail string, sess *session) (int64, error) { @@ -256,3 +258,11 @@ func SearchUsers(query *m.SearchUsersQuery) error { err := sess.Find(&query.Result) return err } + +func DeleteUser(cmd *m.DeleteUserCommand) error { + return inTransaction(func(sess *xorm.Session) error { + var rawSql = fmt.Sprintf("DELETE FROM %s WHERE id=?", x.Dialect().Quote("user")) + _, err := sess.Exec(rawSql, cmd.UserId) + return err + }) +} diff --git a/src/app/features/admin/adminUsersCtrl.js b/src/app/features/admin/adminUsersCtrl.js index ce5cc7a3b86..bf8e998a16f 100644 --- a/src/app/features/admin/adminUsersCtrl.js +++ b/src/app/features/admin/adminUsersCtrl.js @@ -24,7 +24,9 @@ function (angular) { title: 'Delete user', text: 'Are you sure you want to delete user: ' + user.login, onConfirm: function() { - backendSrv.delete('/api/admin/users/delete/' + user.id); + backendSrv.delete('/api/admin/users/' + user.id).then(function() { + $scope.getUsers(); + }); } }); }; diff --git a/src/app/features/admin/partials/users.html b/src/app/features/admin/partials/users.html index fb59f6ee514..5a5b2af97e0 100644 --- a/src/app/features/admin/partials/users.html +++ b/src/app/features/admin/partials/users.html @@ -33,7 +33,7 @@ Edit    - +
    + + + + + + + + +
    {{secName}}
    {{keyName}}{{keyValue}}
    +
    +
    + + + diff --git a/src/app/features/admin/partials/users.html b/src/app/features/admin/partials/users.html index 5a5b2af97e0..fb86b18eacb 100644 --- a/src/app/features/admin/partials/users.html +++ b/src/app/features/admin/partials/users.html @@ -1,6 +1,6 @@ diff --git a/src/app/features/all.js b/src/app/features/all.js index 6ebabafcbad..635b4b5b527 100644 --- a/src/app/features/all.js +++ b/src/app/features/all.js @@ -9,13 +9,7 @@ define([ './dashboard/all', './panel/all', './profile/profileCtrl', - './account/accountUsersCtrl', - './account/datasourcesCtrl', - './account/apiKeysCtrl', - './account/importCtrl', - './account/accountCtrl', - './admin/adminUsersCtrl', - './admin/adminCtrl', - './admin/adminEditUserCtrl', + './account/all', + './admin/all', './grafanaDatasource/datasource', ], function () {}); diff --git a/src/app/routes/backend/all.js b/src/app/routes/backend/all.js index e9d5d8ac95d..89757b39111 100644 --- a/src/app/routes/backend/all.js +++ b/src/app/routes/backend/all.js @@ -54,9 +54,9 @@ define([ templateUrl: 'app/features/profile/partials/profile.html', controller : 'ProfileCtrl', }) - .when('/admin', { - templateUrl: 'app/features/admin/partials/admin.html', - controller : 'AdminCtrl', + .when('/admin/settings', { + templateUrl: 'app/features/admin/partials/settings.html', + controller : 'AdminSettingsCtrl', }) .when('/admin/users', { templateUrl: 'app/features/admin/partials/users.html', diff --git a/src/css/less/admin.less b/src/css/less/admin.less new file mode 100644 index 00000000000..5345e4242fc --- /dev/null +++ b/src/css/less/admin.less @@ -0,0 +1,10 @@ + +.admin-settings-section { + color: @variable; + font-weight: bold; +} + +td.admin-settings-key { + padding-left: 20px; +} + diff --git a/src/css/less/grafana.less b/src/css/less/grafana.less index 5fbfa053b6d..2d8b5604726 100644 --- a/src/css/less/grafana.less +++ b/src/css/less/grafana.less @@ -12,6 +12,7 @@ @import "navbar.less"; @import "gfbox.less"; @import "dashlist.less"; +@import "admin.less"; .row-control-inner { padding:0px; From 1a440361485aa7128d43e06c27ab6fbc92398ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 13 Feb 2015 07:26:33 +0100 Subject: [PATCH 028/118] Fixed req.Host in datasource proxy, Fixes #1478 --- pkg/api/dataproxy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 636fbe4d1fa..631322b93e2 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -17,6 +17,7 @@ func NewReverseProxy(ds *m.DataSource, proxyPath string) *httputil.ReverseProxy director := func(req *http.Request) { req.URL.Scheme = target.Scheme req.URL.Host = target.Host + req.Host = target.Host reqQueryVals := req.URL.Query() From 8722ee8ad6a771deb0025afdde3cf36cc27ad580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 13 Feb 2015 08:47:44 +0100 Subject: [PATCH 029/118] Worked on playlist update for Grafana 2.0, added dashboard search to playliststart view, #1460 --- pkg/api/admin_settings.go | 1 + src/app/features/dashboard/playlistCtrl.js | 31 ++++-- src/app/features/dashboard/playlistSrv.js | 8 +- src/app/partials/playlist.html | 118 +++++++++++++-------- src/app/services/userSrv.js | 24 ----- 5 files changed, 102 insertions(+), 80 deletions(-) delete mode 100644 src/app/services/userSrv.js diff --git a/pkg/api/admin_settings.go b/pkg/api/admin_settings.go index c126c1108c6..21615219acd 100644 --- a/pkg/api/admin_settings.go +++ b/pkg/api/admin_settings.go @@ -20,6 +20,7 @@ func AdminGetSettings(c *middleware.Context) { if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") { value = "************" } + jsonSec[keyName] = value } } diff --git a/src/app/features/dashboard/playlistCtrl.js b/src/app/features/dashboard/playlistCtrl.js index 9e6a60013e2..357663b722d 100644 --- a/src/app/features/dashboard/playlistCtrl.js +++ b/src/app/features/dashboard/playlistCtrl.js @@ -8,29 +8,38 @@ function (angular, _, config) { var module = angular.module('grafana.controllers'); - module.controller('PlaylistCtrl', function($scope, playlistSrv) { + module.controller('PlaylistCtrl', function($scope, playlistSrv, datasourceSrv) { $scope.init = function() { + $scope.playlist = []; $scope.timespan = config.playlist_timespan; - $scope.loadFavorites(); + $scope.db = datasourceSrv.getGrafanaDB(); + $scope.search(); }; - $scope.loadFavorites = function() { - $scope.favDashboards = playlistSrv.getFavorites().dashboards; + $scope.search = function() { + var query = {starred: true, limit: 10}; - _.each($scope.favDashboards, function(dashboard) { - dashboard.include = true; + if ($scope.searchQuery) { + query.query = $scope.searchQuery; + query.starred = false; + } + + $scope.db.searchDashboards(query).then(function(results) { + $scope.searchHits = results.dashboards; }); }; - $scope.removeAsFavorite = function(dashboard) { - playlistSrv.removeAsFavorite(dashboard); - $scope.loadFavorites(); + $scope.addDashboard = function(dashboard) { + $scope.playlist.push(dashboard); + }; + + $scope.removeDashboard = function(dashboard) { + $scope.playlist = _.without($scope.playlist, dashboard); }; $scope.start = function() { - var included = _.where($scope.favDashboards, { include: true }); - playlistSrv.start(included, $scope.timespan); + playlistSrv.start($scope.playlist, $scope.timespan); }; }); diff --git a/src/app/features/dashboard/playlistSrv.js b/src/app/features/dashboard/playlistSrv.js index b486e5957fc..1af3360e85c 100644 --- a/src/app/features/dashboard/playlistSrv.js +++ b/src/app/features/dashboard/playlistSrv.js @@ -61,6 +61,8 @@ function (angular, _, kbn, store) { }; this.start = function(dashboards, timespan) { + this.stop(); + var interval = kbn.interval_to_ms(timespan); var index = 0; @@ -69,8 +71,10 @@ function (angular, _, kbn, store) { timerInstance = setInterval(function() { $rootScope.$apply(function() { angular.element(window).unbind('resize'); - $location.search({}); - $location.path(dashboards[index % dashboards.length].url); + var dash = dashboards[index % dashboards.length]; + var relativeUrl = dash.url.substring($location.absUrl().length - $location.url().length); + $location.url(relativeUrl); + index++; }); }, interval); diff --git a/src/app/partials/playlist.html b/src/app/partials/playlist.html index c0b9f0545fb..05d0422771b 100644 --- a/src/app/partials/playlist.html +++ b/src/app/partials/playlist.html @@ -12,56 +12,88 @@
    - -
    -
    -
    - - - - - - - - - - - - - - -
    DashboardIncludeRemove as favorite
    - {{dashboard.title}} - - - - - -
    - No dashboards marked as favorites -
    -
    -
    -
    - - dashboards available in the playlist are only the ones marked as favorites (stored in local browser storage). - to mark a dashboard as favorite, use save icon in the menu and in the dropdown select mark as favorite -

    -
    +
    +
    +
    +
    +
      +
    • + Search +
    • +
    • + +
    • +
    +
    -
    - - -
    +
    +
    Playlist dashboards
    +
    +
    +
    +
    + + + + + + + + +
    + {{dashboard.title}} + + +
    + No dashboards found +
    +
    +
    + + + + + + + + +
    + {{dashboard.title}} + + +
    + No dashboards found +
    +

    -
    +
    +
    +
      +
    • + Timespan between dashboard change +
    • +
    • + +
    • +
    • + +
    • +
    +
    +
    +
    + +
    +
    diff --git a/src/app/services/userSrv.js b/src/app/services/userSrv.js deleted file mode 100644 index 14f373f7b1b..00000000000 --- a/src/app/services/userSrv.js +++ /dev/null @@ -1,24 +0,0 @@ -define([ - 'angular', - 'lodash', -], -function (angular, _) { - 'use strict'; - - var module = angular.module('grafana.services'); - - module.service('userSrv', function() { - - function User() { - if (window.grafanaBootData.user) { - _.extend(this, window.grafanaBootData.user); - } - } - - this.getSignedInUser = function() { - return new User(); - }; - - }); - -}); From 8aef2c13ec8980fdf889f53ff8e4ca66638829b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 13 Feb 2015 09:31:40 +0100 Subject: [PATCH 030/118] Worked on playlist mode, hides dashboard controls and dashboard search button, added prev, stop, next icon buttons --- src/app/directives/bodyClass.js | 8 +- src/app/features/dashboard/playlistSrv.js | 93 +++++++---------------- src/app/partials/dashboard_topnav.html | 26 ++++--- src/css/less/grafana.less | 8 +- 4 files changed, 50 insertions(+), 85 deletions(-) diff --git a/src/app/directives/bodyClass.js b/src/app/directives/bodyClass.js index 0b1cac65614..20339b8bf92 100644 --- a/src/app/directives/bodyClass.js +++ b/src/app/directives/bodyClass.js @@ -1,9 +1,8 @@ define([ 'angular', - 'app', 'lodash' ], -function (angular) { +function (angular, _) { 'use strict'; angular @@ -35,9 +34,8 @@ function (angular) { } }); - $scope.$watch('playlist_active', function() { - elem.toggleClass('hide-controls', $scope.playlist_active === true); - elem.toggleClass('playlist-active', $scope.playlist_active === true); + $scope.$watch('playlistSrv', function(newValue) { + elem.toggleClass('playlist-active', _.isObject(newValue)); }); } }; diff --git a/src/app/features/dashboard/playlistSrv.js b/src/app/features/dashboard/playlistSrv.js index 1af3360e85c..2c10391124b 100644 --- a/src/app/features/dashboard/playlistSrv.js +++ b/src/app/features/dashboard/playlistSrv.js @@ -4,88 +4,51 @@ define([ 'kbn', 'store' ], -function (angular, _, kbn, store) { +function (angular, _, kbn) { 'use strict'; var module = angular.module('grafana.services'); - module.service('playlistSrv', function($location, $rootScope) { - var timerInstance; - var favorites = { dashboards: [] }; + module.service('playlistSrv', function($location, $rootScope, $timeout) { + var self = this; - this.init = function() { - var existingJson = store.get("grafana-favorites"); - if (existingJson) { - favorites = angular.fromJson(existingJson); - } + this.next = function() { + $timeout.cancel(self.cancelPromise); + + angular.element(window).unbind('resize'); + var dash = self.dashboards[self.index % self.dashboards.length]; + var relativeUrl = dash.url.substring($location.absUrl().length - $location.url().length); + + $location.url(relativeUrl); + self.index++; + + self.cancelPromise = $timeout(self.next, self.interval); }; - this._save = function() { - store.set('grafana-favorites', angular.toJson(favorites)); - }; - - this._find = function(title) { - return _.findWhere(favorites.dashboards, { title: title }); - }; - - this._remove = function(existing) { - if (existing) { - favorites.dashboards = _.without(favorites.dashboards, existing); - } - }; - - this.isCurrentFavorite = function(dashboard) { - return this._find(dashboard.title) ? true : false; - }; - - this.markAsFavorite = function(dashboard) { - var existing = this._find(dashboard.title); - this._remove(existing); - - favorites.dashboards.push({ - url: $location.path(), - title: dashboard.title - }); - - this._save(); - }; - - this.removeAsFavorite = function(toRemove) { - var existing = this._find(toRemove.title); - this._remove(existing); - this._save(); - }; - - this.getFavorites = function() { - return favorites; + this.prev = function() { + self.index = Math.max(self.index - 2, 0); + self.next(); }; this.start = function(dashboards, timespan) { this.stop(); - var interval = kbn.interval_to_ms(timespan); - var index = 0; + self.interval = kbn.interval_to_ms(timespan); + self.dashboards = dashboards; + $rootScope.playlistSrv = this; - $rootScope.playlist_active = true; - - timerInstance = setInterval(function() { - $rootScope.$apply(function() { - angular.element(window).unbind('resize'); - var dash = dashboards[index % dashboards.length]; - var relativeUrl = dash.url.substring($location.absUrl().length - $location.url().length); - $location.url(relativeUrl); - - index++; - }); - }, interval); + self.cancelPromise = $timeout(self.next, self.interval); }; this.stop = function() { - clearInterval(timerInstance); - $rootScope.playlist_active = false; - }; + self.index = 0; - this.init(); + if (self.cancelPromise) { + $timeout.cancel(self.cancelPromise); + } + + $rootScope.playlistSrv = null; + }; }); diff --git a/src/app/partials/dashboard_topnav.html b/src/app/partials/dashboard_topnav.html index 38f367ff87f..71956a8c461 100644 --- a/src/app/partials/dashboard_topnav.html +++ b/src/app/partials/dashboard_topnav.html @@ -19,11 +19,11 @@ - {{dashboard.title}} - -
    + {{dashboard.title}} + +
    -
    diff --git a/src/css/less/grafana.less b/src/css/less/grafana.less index 2d8b5604726..7b32d09e854 100644 --- a/src/css/less/grafana.less +++ b/src/css/less/grafana.less @@ -35,13 +35,11 @@ .playlist-active { .grafana-menu-zoom-out, - .grafana-menu-save, - .grafana-menu-load, .add-row-panel-hint, - .grafana-menu-home, .grafana-menu-refresh, - .grafana-menu-edit { - display: none; + .top-nav-dashboards-btn, + .top-nav-dash-actions { + display:none; } .grafana-menu-stop-playlist { From 1d86d4b94f9bfc7154c4b5c1cc947a89ca6431f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 13 Feb 2015 14:18:37 +0100 Subject: [PATCH 031/118] Updated docker build, added test container script, added docker readme --- docker/production/README.md | 31 +++++++++++++++++++++++++++++ docker/production/build.sh | 2 +- docker/production/test_container.sh | 5 +++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 docker/production/README.md create mode 100755 docker/production/test_container.sh diff --git a/docker/production/README.md b/docker/production/README.md new file mode 100644 index 00000000000..f6da10e62f6 --- /dev/null +++ b/docker/production/README.md @@ -0,0 +1,31 @@ + +# Grafana docker image + +This container currently only contains the in development alpha of Grafana 2.0 (ie non production use). The +`#develop` tag is constantly updated as we make progress torwards a beta release. + + +## Running your Grafana image +-------------------------- + +Start your image binding the external port `3000`. + + docker run -i -p 3000:3000 grafana/grafana + +Try it out, default admin user is admin/admin. + + +## Configuring your Grafana container + +All options defined in conf/grafana.ini can be overriden using environment variables, for example: + + +``` +docker run -i -p 3000:3000 \ + -e "GF_SERVER_ROOT_URL=http://grafana.server.name" \ + -e "GF_SECURITY_ADMIN_PASSWORD=secret \ + grafana/grafana:develop +``` + + + diff --git a/docker/production/build.sh b/docker/production/build.sh index 4179d8e032e..d97b29de934 100755 --- a/docker/production/build.sh +++ b/docker/production/build.sh @@ -5,7 +5,7 @@ cd ../../ go run build.go build -npm grunt release +grunt release docker build --tag "grafana/grafana:develop" . diff --git a/docker/production/test_container.sh b/docker/production/test_container.sh new file mode 100755 index 00000000000..aa8fdba4cbf --- /dev/null +++ b/docker/production/test_container.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +docker run -i -p 3001:3000 \ + -e "GF_SERVER_ROOT_URL=http://grafana.server.name" \ + grafana/grafana:develop From 6263ec1d3892312ae126277c9d301d36b6c46777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 13 Feb 2015 14:36:16 +0100 Subject: [PATCH 032/118] Fixed some playlist issues, error when adding duplicates --- src/app/features/dashboard/playlistCtrl.js | 9 +++++++++ src/app/partials/playlist.html | 7 +------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/app/features/dashboard/playlistCtrl.js b/src/app/features/dashboard/playlistCtrl.js index 357663b722d..50440c5912b 100644 --- a/src/app/features/dashboard/playlistCtrl.js +++ b/src/app/features/dashboard/playlistCtrl.js @@ -27,15 +27,24 @@ function (angular, _, config) { $scope.db.searchDashboards(query).then(function(results) { $scope.searchHits = results.dashboards; + $scope.filterHits(); + }); + }; + + $scope.filterHits = function() { + $scope.filteredHits = _.reject($scope.searchHits, function(dash) { + return _.findWhere($scope.playlist, {slug: dash.slug}); }); }; $scope.addDashboard = function(dashboard) { $scope.playlist.push(dashboard); + $scope.filterHits(); }; $scope.removeDashboard = function(dashboard) { $scope.playlist = _.without($scope.playlist, dashboard); + $scope.filterHits(); }; $scope.start = function() { diff --git a/src/app/partials/playlist.html b/src/app/partials/playlist.html index 05d0422771b..38fd552d43b 100644 --- a/src/app/partials/playlist.html +++ b/src/app/partials/playlist.html @@ -36,7 +36,7 @@
    - + @@ -65,11 +65,6 @@ - - -
    {{dashboard.title}}
    - No dashboards found -
    From 55ba8ad0c4a5e5913769a67f19f234ae47241b1a Mon Sep 17 00:00:00 2001 From: woodsaj Date: Fri, 13 Feb 2015 21:52:56 +0800 Subject: [PATCH 033/118] fix issue 1481. correctly escape api_key.key column name 'key' is a reserved word in mysql. So when building a query, the api-key.key column name needs to be escaped --- pkg/services/sqlstore/apikey.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/apikey.go b/pkg/services/sqlstore/apikey.go index 458548bd680..5db8586ccb3 100644 --- a/pkg/services/sqlstore/apikey.go +++ b/pkg/services/sqlstore/apikey.go @@ -66,7 +66,7 @@ func UpdateApiKey(cmd *m.UpdateApiKeyCommand) error { func GetApiKeyByKey(query *m.GetApiKeyByKeyQuery) error { var apikey m.ApiKey - has, err := x.Where("key=?", query.Key).Get(&apikey) + has, err := x.Where("`key`=?", query.Key).Get(&apikey) if err != nil { return err From 7d69885e060ff6aa9ca4fb0586386e8f8ab753e4 Mon Sep 17 00:00:00 2001 From: woodsaj Date: Fri, 13 Feb 2015 21:52:56 +0800 Subject: [PATCH 034/118] fixes #1481. correctly escape api_key.key column name 'key' is a reserved word in mysql. So when building a query, the api-key.key column name needs to be escaped --- pkg/services/sqlstore/apikey.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/apikey.go b/pkg/services/sqlstore/apikey.go index 458548bd680..5db8586ccb3 100644 --- a/pkg/services/sqlstore/apikey.go +++ b/pkg/services/sqlstore/apikey.go @@ -66,7 +66,7 @@ func UpdateApiKey(cmd *m.UpdateApiKeyCommand) error { func GetApiKeyByKey(query *m.GetApiKeyByKeyQuery) error { var apikey m.ApiKey - has, err := x.Where("key=?", query.Key).Get(&apikey) + has, err := x.Where("`key`=?", query.Key).Get(&apikey) if err != nil { return err From a88187023dc1d704e92255c34b268bf267f793ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 13 Feb 2015 15:55:32 +0100 Subject: [PATCH 035/118] Added a sql integration test for api keys --- pkg/services/sqlstore/apikey_test.go | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 pkg/services/sqlstore/apikey_test.go diff --git a/pkg/services/sqlstore/apikey_test.go b/pkg/services/sqlstore/apikey_test.go new file mode 100644 index 00000000000..de31b6c1d1d --- /dev/null +++ b/pkg/services/sqlstore/apikey_test.go @@ -0,0 +1,31 @@ +package sqlstore + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" + + m "github.com/grafana/grafana/pkg/models" +) + +func TestApiKeyDataAccess(t *testing.T) { + + Convey("Testing API Key data access", t, func() { + InitTestDB(t) + + Convey("Given saved api key", func() { + cmd := m.AddApiKeyCommand{AccountId: 1, Key: "hello"} + err := AddApiKey(&cmd) + So(err, ShouldBeNil) + + Convey("Should be able to get key by key", func() { + query := m.GetApiKeyByKeyQuery{Key: "hello"} + err = GetApiKeyByKey(&query) + So(err, ShouldBeNil) + + So(query.Result, ShouldNotBeNil) + }) + + }) + }) +} From 0767992662270e3a0138305c7d26114a1e33d1eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 14 Feb 2015 08:40:15 +0100 Subject: [PATCH 036/118] fixed z-index pos for panel-menu compared to dashboard search dropdown, #1483 --- src/css/less/panel.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/css/less/panel.less b/src/css/less/panel.less index e1740120ad9..e0b01ed43dc 100644 --- a/src/css/less/panel.less +++ b/src/css/less/panel.less @@ -121,7 +121,7 @@ } .panel-menu { - z-index: 1000; + z-index: 500; position: absolute; background: @grafanaTargetFuncBackground; border: 1px solid black; From ed0fabd9de873bd10d028eb97659f88db256ebb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 14 Feb 2015 09:05:35 +0100 Subject: [PATCH 037/118] Fixed css issue with sidenav not extending to bottom of page when dashboard requires scrolling, #1483 --- src/css/less/sidemenu.less | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/css/less/sidemenu.less b/src/css/less/sidemenu.less index b235c10de1b..a90224c9fa9 100644 --- a/src/css/less/sidemenu.less +++ b/src/css/less/sidemenu.less @@ -1,7 +1,12 @@ +.sidemenu-canvas { + position: relative; +} + .sidemenu-wrapper { position: absolute; display: none; top: 0; + bottom: 0; left: 0; width: 200px; background: @bodyBackground; From f6c07fdabda1e792213e7896df163cef1da0d65c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 14 Feb 2015 10:04:27 +0100 Subject: [PATCH 038/118] Rewrote and redesign how the data source edit views look and work so they conform better to how account views look, removed tabs and put top nav items to add data source etc, made list, edit and new seperate url routes, #1483 --- pkg/api/api.go | 1 + pkg/api/datasources.go | 31 +++- src/app/controllers/sidemenuCtrl.js | 2 +- src/app/features/account/all.js | 1 + .../features/account/datasourceEditCtrl.js | 69 ++++++++ src/app/features/account/datasourcesCtrl.js | 58 +------ .../account/partials/datasourceEdit.html | 74 +++++++++ .../account/partials/datasources.html | 155 +++++------------- src/app/routes/backend/all.js | 8 + 9 files changed, 229 insertions(+), 170 deletions(-) create mode 100644 src/app/features/account/datasourceEditCtrl.js create mode 100644 src/app/features/account/partials/datasourceEdit.html diff --git a/pkg/api/api.go b/pkg/api/api.go index 28e2ea8ab3d..f79c0661d55 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -75,6 +75,7 @@ func Register(r *macaron.Macaron) { r.Group("/datasources", func() { r.Combo("/").Get(GetDataSources).Put(AddDataSource).Post(UpdateDataSource) r.Delete("/:id", DeleteDataSource) + r.Get("/:id", GetDataSourceById) }, reqAccountAdmin) r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index c4a7ae4d657..33a7bac2667 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -9,9 +9,8 @@ import ( func GetDataSources(c *middleware.Context) { query := m.GetDataSourcesQuery{AccountId: c.AccountId} - err := bus.Dispatch(&query) - if err != nil { + if err := bus.Dispatch(&query); err != nil { c.JsonApiErr(500, "Failed to query datasources", err) return } @@ -36,6 +35,34 @@ func GetDataSources(c *middleware.Context) { c.JSON(200, result) } +func GetDataSourceById(c *middleware.Context) { + query := m.GetDataSourceByIdQuery{ + Id: c.ParamsInt64(":id"), + AccountId: c.AccountId, + } + + if err := bus.Dispatch(&query); err != nil { + c.JsonApiErr(500, "Failed to query datasources", err) + return + } + + ds := query.Result + + c.JSON(200, &dtos.DataSource{ + Id: ds.Id, + AccountId: ds.AccountId, + Name: ds.Name, + Url: ds.Url, + Type: ds.Type, + Access: ds.Access, + Password: ds.Password, + Database: ds.Database, + User: ds.User, + BasicAuth: ds.BasicAuth, + IsDefault: ds.IsDefault, + }) +} + func DeleteDataSource(c *middleware.Context) { id := c.ParamsInt64(":id") diff --git a/src/app/controllers/sidemenuCtrl.js b/src/app/controllers/sidemenuCtrl.js index 211ca4451c0..184e2cab2fb 100644 --- a/src/app/controllers/sidemenuCtrl.js +++ b/src/app/controllers/sidemenuCtrl.js @@ -37,7 +37,7 @@ function (angular, _, $, config) { if (contextSrv.user.isGrafanaAdmin) { $scope.menu.push({ - text: "Admin", href: $scope.getUrl("/admin/users"), + text: "Admin", href: $scope.getUrl("/admin/settings"), icon: "fa fa-cube", requireSignedIn: true, }); diff --git a/src/app/features/account/all.js b/src/app/features/account/all.js index 7c01d7b8c1c..277ae1a75f1 100644 --- a/src/app/features/account/all.js +++ b/src/app/features/account/all.js @@ -1,6 +1,7 @@ define([ './accountUsersCtrl', './datasourcesCtrl', + './datasourceEditCtrl', './apiKeysCtrl', './importCtrl', './accountCtrl', diff --git a/src/app/features/account/datasourceEditCtrl.js b/src/app/features/account/datasourceEditCtrl.js new file mode 100644 index 00000000000..bf1912f2677 --- /dev/null +++ b/src/app/features/account/datasourceEditCtrl.js @@ -0,0 +1,69 @@ +define([ + 'angular', + 'lodash', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('DataSourceEditCtrl', function($scope, $http, backendSrv, $routeParams, $location) { + + var defaults = { + name: '', + type: 'graphite', + url: '', + access: 'proxy' + }; + + $scope.types = [ + { name: 'Graphite', type: 'graphite' }, + { name: 'InfluxDB', type: 'influxdb' }, + { name: 'Elasticsearch', type: 'elasticsearch' }, + { name: 'OpenTSDB', type: 'opentsdb' }, + ]; + + $scope.init = function() { + $scope.isNew = true; + $scope.datasources = []; + + if ($routeParams.id) { + $scope.isNew = false; + $scope.getDatasourceById($routeParams.id); + } else { + $scope.current = angular.copy(defaults); + } + }; + + $scope.getDatasourceById = function(id) { + backendSrv.get('/api/datasources/' + id).then(function(ds) { + $scope.current = ds; + }); + }; + + $scope.update = function() { + if (!$scope.editForm.$valid) { + return; + } + + backendSrv.post('/api/datasources', $scope.current).then(function() { + $location.path("account/datasources"); + }); + }; + + $scope.add = function() { + if (!$scope.editForm.$valid) { + return; + } + + backendSrv.put('/api/datasources', $scope.current) + .then(function() { + $scope.editor.index = 0; + $scope.getDatasources(); + }); + }; + + $scope.init(); + + }); +}); diff --git a/src/app/features/account/datasourcesCtrl.js b/src/app/features/account/datasourcesCtrl.js index b8f9f9fda7a..00a906ba64e 100644 --- a/src/app/features/account/datasourcesCtrl.js +++ b/src/app/features/account/datasourcesCtrl.js @@ -1,5 +1,6 @@ define([ 'angular', + 'lodash', ], function (angular) { 'use strict'; @@ -8,47 +9,9 @@ function (angular) { module.controller('DataSourcesCtrl', function($scope, $http, backendSrv) { - var defaults = { - name: '', - type: 'graphite', - url: '', - access: 'proxy' - }; - - $scope.types = [ - { name: 'Graphite', type: 'graphite' }, - { name: 'InfluxDB', type: 'influxdb' }, - { name: 'Elasticsearch', type: 'elasticsearch' }, - { name: 'OpenTSDB', type: 'opentsdb' }, - ]; - $scope.init = function() { - $scope.reset(); - $scope.editor = {index: 0}; $scope.datasources = []; $scope.getDatasources(); - - $scope.$watch('editor.index', function(newVal) { - if (newVal !== 2) { - $scope.reset(); - } - }); - }; - - $scope.reset = function() { - $scope.current = angular.copy(defaults); - $scope.currentIsNew = true; - }; - - $scope.edit = function(ds) { - $scope.current = ds; - $scope.currentIsNew = false; - $scope.editor.index = 2; - }; - - $scope.cancel = function() { - $scope.reset(); - $scope.editor.index = 0; }; $scope.getDatasources = function() { @@ -63,25 +26,6 @@ function (angular) { }); }; - $scope.update = function() { - backendSrv.post('/api/datasources', $scope.current).then(function() { - $scope.editor.index = 0; - $scope.getDatasources(); - }); - }; - - $scope.add = function() { - if (!$scope.editForm.$valid) { - return; - } - - backendSrv.put('/api/datasources', $scope.current) - .then(function() { - $scope.editor.index = 0; - $scope.getDatasources(); - }); - }; - $scope.init(); }); diff --git a/src/app/features/account/partials/datasourceEdit.html b/src/app/features/account/partials/datasourceEdit.html new file mode 100644 index 00000000000..fc8b865974f --- /dev/null +++ b/src/app/features/account/partials/datasourceEdit.html @@ -0,0 +1,74 @@ + + + + +
    +
    +

    Add data source

    +

    Edit {{current.name}}

    + +
    +
    +
    + + +
    +
    + + +
    + +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    InfluxDB Details
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    +
    +
    Elastic search details
    +
    + + +
    +
    +
    + +
    + + + + Cancel +
    +
    +
    +
    diff --git a/src/app/features/account/partials/datasources.html b/src/app/features/account/partials/datasources.html index f3bac8b35c6..25bf63d6751 100644 --- a/src/app/features/account/partials/datasources.html +++ b/src/app/features/account/partials/datasources.html @@ -1,117 +1,52 @@ - + + -
    -
    -
    -
    -
    +
    +
    +

    Data sources

    + +
    + No datasources defined
    -
    -
    -
    -
    -
    -
    - No datasources defined -
    - - - - - - - - - - - - - - - -
    NameUrl
    -   - {{ds.name}} - - {{ds.url}} - - - default - - - - - Edit - - - - - -
    -
    -
    + + + + + + + + + + + + + + + +
    NameUrl
    +   + {{ds.name}} + + {{ds.url}} + + + default + + + + + Edit + + + + + +
    -
    -
    -
    - - -
    -
    - - -
    - -
    - -
    -
    - - -
    -
    - - -
    -
    - -
    -
    -
    InfluxDB Details
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    -
    -
    -
    Elastic search details
    -
    - - -
    -
    -
    -
    - -
    - - - -
    -
    - diff --git a/src/app/routes/backend/all.js b/src/app/routes/backend/all.js index 89757b39111..b211550d841 100644 --- a/src/app/routes/backend/all.js +++ b/src/app/routes/backend/all.js @@ -38,6 +38,14 @@ define([ templateUrl: 'app/features/account/partials/datasources.html', controller : 'DataSourcesCtrl', }) + .when('/account/datasources/edit/:id', { + templateUrl: 'app/features/account/partials/datasourceEdit.html', + controller : 'DataSourceEditCtrl', + }) + .when('/account/datasources/new', { + templateUrl: 'app/features/account/partials/datasourceEdit.html', + controller : 'DataSourceEditCtrl', + }) .when('/account/users', { templateUrl: 'app/features/account/partials/users.html', controller : 'AccountUsersCtrl', From ca37b2445567b149f9e86ffc656f0497f44dec03 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Sat, 14 Feb 2015 15:31:37 -0700 Subject: [PATCH 039/118] CLI: Add account list command --- main.go | 3 +- pkg/cmd/accounts.go | 47 ++++++++++++++++++++++++++++++++ pkg/models/account.go | 4 +++ pkg/services/sqlstore/account.go | 5 ++++ 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 pkg/cmd/accounts.go diff --git a/main.go b/main.go index 997fbd38f0b..6f753adc185 100644 --- a/main.go +++ b/main.go @@ -31,7 +31,8 @@ func main() { app.Name = "Grafana Backend" app.Usage = "grafana web" app.Version = version - app.Commands = []cli.Command{cmd.CmdWeb, cmd.CmdImportJson} + app.Commands = []cli.Command{cmd.CmdWeb, cmd.CmdImportJson, + cmd.CmdListAccounts} app.Flags = append(app.Flags, []cli.Flag{}...) app.Run(os.Args) diff --git a/pkg/cmd/accounts.go b/pkg/cmd/accounts.go new file mode 100644 index 00000000000..209e10f0eaa --- /dev/null +++ b/pkg/cmd/accounts.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "fmt" + "github.com/codegangsta/cli" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" + "os" + "text/tabwriter" +) + +var CmdListAccounts = cli.Command{ + Name: "account", + Usage: "list accounts", + Description: "Lists the accounts in the system", + Action: listAccounts, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "config", + Value: "grafana.ini", + Usage: "path to config file", + }, + }, +} + +func listAccounts(c *cli.Context) { + setting.NewConfigContext() + sqlstore.NewEngine() + sqlstore.EnsureAdminUser() + + accountsQuery := m.GetAccountsQuery{} + if err := bus.Dispatch(&accountsQuery); err != nil { + log.Error(3, "Failed to find accounts", err) + return + } + + w := tabwriter.NewWriter(os.Stdout, 20, 1, 4, ' ', 0) + + fmt.Fprintf(w, "ID\tNAME\n") + for _, account := range accountsQuery.Result { + fmt.Fprintf(w, "%d\t%s\n", account.Id, account.Name) + } + w.Flush() +} diff --git a/pkg/models/account.go b/pkg/models/account.go index ad4cdc47e53..12696cc962d 100644 --- a/pkg/models/account.go +++ b/pkg/models/account.go @@ -49,6 +49,10 @@ type GetAccountByNameQuery struct { Result *Account } +type GetAccountsQuery struct { + Result []*Account +} + type AccountDTO struct { Id int64 `json:"id"` Name string `json:"name"` diff --git a/pkg/services/sqlstore/account.go b/pkg/services/sqlstore/account.go index ff1f2fa0546..7b0e2bb96b6 100644 --- a/pkg/services/sqlstore/account.go +++ b/pkg/services/sqlstore/account.go @@ -14,6 +14,11 @@ func init() { bus.AddHandler("sql", SetUsingAccount) bus.AddHandler("sql", UpdateAccount) bus.AddHandler("sql", GetAccountByName) + bus.AddHandler("sql", GetAccountsQuery) +} + +func GetAccountsQuery(query *m.GetAccountsQuery) error { + return x.Find(&query.Result) } func GetAccountById(query *m.GetAccountByIdQuery) error { From 7d4c319fcb7b9b76a65688d3e921aa428bd508a5 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Sat, 14 Feb 2015 15:34:06 -0700 Subject: [PATCH 040/118] CLI: Default logging output to stderr instead of stdout Makes it possible to separate console output from logging output so command output can be piped to a file cleanly. --- pkg/log/console.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/log/console.go b/pkg/log/console.go index f5a8b96fd76..44e53e50d75 100644 --- a/pkg/log/console.go +++ b/pkg/log/console.go @@ -40,7 +40,7 @@ type ConsoleWriter struct { // create ConsoleWriter returning as LoggerInterface. func NewConsole() LoggerInterface { return &ConsoleWriter{ - lg: log.New(os.Stdout, "", log.Ldate|log.Ltime), + lg: log.New(os.Stderr, "", log.Ldate|log.Ltime), Level: TRACE, } } From 81531a29eb9e3d6298f18405f350e06b28136736 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Sun, 15 Feb 2015 10:08:55 -0700 Subject: [PATCH 041/118] CLI: Add account:create command Creates a new account attached to the default admin account for now. --- main.go | 2 +- pkg/cmd/accounts.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 6f753adc185..83a83412ee7 100644 --- a/main.go +++ b/main.go @@ -32,7 +32,7 @@ func main() { app.Usage = "grafana web" app.Version = version app.Commands = []cli.Command{cmd.CmdWeb, cmd.CmdImportJson, - cmd.CmdListAccounts} + cmd.CmdListAccounts, cmd.CmdCreateAccount} app.Flags = append(app.Flags, []cli.Flag{}...) app.Run(os.Args) diff --git a/pkg/cmd/accounts.go b/pkg/cmd/accounts.go index 209e10f0eaa..a2eea8b6609 100644 --- a/pkg/cmd/accounts.go +++ b/pkg/cmd/accounts.go @@ -26,6 +26,20 @@ var CmdListAccounts = cli.Command{ }, } +var CmdCreateAccount = cli.Command{ + Name: "account:create", + Usage: "create a new account", + Description: "Creates a new account", + Action: createAccount, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "config", + Value: "grafana.ini", + Usage: "path to config file", + }, + }, +} + func listAccounts(c *cli.Context) { setting.NewConfigContext() sqlstore.NewEngine() @@ -45,3 +59,32 @@ func listAccounts(c *cli.Context) { } w.Flush() } + +func createAccount(c *cli.Context) { + setting.NewConfigContext() + sqlstore.NewEngine() + sqlstore.EnsureAdminUser() + + if !c.Args().Present() { + fmt.Printf("Account name arg is required\n") + return + } + + name := c.Args().First() + + adminQuery := m.GetUserByLoginQuery{LoginOrEmail: setting.AdminUser} + + if err := bus.Dispatch(&adminQuery); err == m.ErrUserNotFound { + log.Error(3, "Failed to find default admin user", err) + return + } + + adminUser := adminQuery.Result + + cmd := m.CreateAccountCommand{Name: name, UserId: adminUser.Id} + if err := bus.Dispatch(&cmd); err != nil { + log.Error(3, "Failed to create account", err) + return + } + fmt.Printf("Account %s created for admin user %s\n", name, adminUser.Email) +} From c1d4acc01e5ab5b2606a101ebbfe4c1dadde697b Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Sun, 15 Feb 2015 10:48:34 -0700 Subject: [PATCH 042/118] CLI: Use colorized console output This extracts some of the colored logging functionality into some convenience functions to log directly to the console (stdout) w/o the usual logging prefixes and flags. It's intended for console messages when using grafana commands. --- pkg/cmd/accounts.go | 15 +++---- pkg/log/console.go | 100 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 97 insertions(+), 18 deletions(-) diff --git a/pkg/cmd/accounts.go b/pkg/cmd/accounts.go index a2eea8b6609..e6742da6fbf 100644 --- a/pkg/cmd/accounts.go +++ b/pkg/cmd/accounts.go @@ -47,8 +47,7 @@ func listAccounts(c *cli.Context) { accountsQuery := m.GetAccountsQuery{} if err := bus.Dispatch(&accountsQuery); err != nil { - log.Error(3, "Failed to find accounts", err) - return + log.ConsoleFatalf("Failed to find accounts: %s", err) } w := tabwriter.NewWriter(os.Stdout, 20, 1, 4, ' ', 0) @@ -66,8 +65,7 @@ func createAccount(c *cli.Context) { sqlstore.EnsureAdminUser() if !c.Args().Present() { - fmt.Printf("Account name arg is required\n") - return + log.ConsoleFatal("Account name arg is required") } name := c.Args().First() @@ -75,16 +73,15 @@ func createAccount(c *cli.Context) { adminQuery := m.GetUserByLoginQuery{LoginOrEmail: setting.AdminUser} if err := bus.Dispatch(&adminQuery); err == m.ErrUserNotFound { - log.Error(3, "Failed to find default admin user", err) - return + log.ConsoleFatalf("Failed to find default admin user: %s", err) } adminUser := adminQuery.Result cmd := m.CreateAccountCommand{Name: name, UserId: adminUser.Id} if err := bus.Dispatch(&cmd); err != nil { - log.Error(3, "Failed to create account", err) - return + log.ConsoleFatalf("Failed to create account: %s", err) } - fmt.Printf("Account %s created for admin user %s\n", name, adminUser.Email) + + log.ConsoleInfof("Account %s created for admin user %s\n", name, adminUser.Email) } diff --git a/pkg/log/console.go b/pkg/log/console.go index 44e53e50d75..2a6bbf6a3bb 100644 --- a/pkg/log/console.go +++ b/pkg/log/console.go @@ -6,6 +6,7 @@ package log import ( "encoding/json" + "fmt" "log" "os" "runtime" @@ -21,15 +22,26 @@ func NewBrush(color string) Brush { } } -var colors = []Brush{ - NewBrush("1;36"), // Trace cyan - NewBrush("1;34"), // Debug blue - NewBrush("1;32"), // Info green - NewBrush("1;33"), // Warn yellow - NewBrush("1;31"), // Error red - NewBrush("1;35"), // Critical purple - NewBrush("1;31"), // Fatal red -} +var ( + Red = NewBrush("1;31") + Purple = NewBrush("1;35") + Yellow = NewBrush("1;33") + Green = NewBrush("1;32") + Blue = NewBrush("1;34") + Cyan = NewBrush("1;36") + + colors = []Brush{ + Cyan, // Trace cyan + Blue, // Debug blue + Green, // Info green + Yellow, // Warn yellow + Red, // Error red + Purple, // Critical purple + Red, // Fatal red + } + consoleWriter = &ConsoleWriter{lg: log.New(os.Stdout, "", 0), + Level: TRACE} +) // ConsoleWriter implements LoggerInterface and writes messages to terminal. type ConsoleWriter struct { @@ -68,6 +80,76 @@ func (_ *ConsoleWriter) Flush() { func (_ *ConsoleWriter) Destroy() { } +func printConsole(level int, msg string) { + consoleWriter.WriteMsg(msg, 0, level) +} + +func printfConsole(level int, format string, v ...interface{}) { + consoleWriter.WriteMsg(fmt.Sprintf(format, v...), 0, level) +} + +// ConsoleTrace prints to stdout using TRACE colors +func ConsoleTrace(s string) { + printConsole(TRACE, s) +} + +// ConsoleTracef prints a formatted string to stdout using TRACE colors +func ConsoleTracef(format string, v ...interface{}) { + printfConsole(TRACE, format, v...) +} + +// ConsoleDebug prints to stdout using DEBUG colors +func ConsoleDebug(s string) { + printConsole(DEBUG, s) +} + +// ConsoleDebugf prints a formatted string to stdout using DEBUG colors +func ConsoleDebugf(format string, v ...interface{}) { + printfConsole(DEBUG, format, v...) +} + +// ConsoleInfo prints to stdout using INFO colors +func ConsoleInfo(s string) { + printConsole(INFO, s) +} + +// ConsoleInfof prints a formatted string to stdout using INFO colors +func ConsoleInfof(format string, v ...interface{}) { + printfConsole(INFO, format, v...) +} + +// ConsoleWarn prints to stdout using WARN colors +func ConsoleWarn(s string) { + printConsole(WARN, s) +} + +// ConsoleWarnf prints a formatted string to stdout using WARN colors +func ConsoleWarnf(format string, v ...interface{}) { + printfConsole(WARN, format, v...) +} + +// ConsoleError prints to stdout using ERROR colors +func ConsoleError(s string) { + printConsole(ERROR, s) +} + +// ConsoleErrorf prints a formatted string to stdout using ERROR colors +func ConsoleErrorf(format string, v ...interface{}) { + printfConsole(ERROR, format, v...) +} + +// ConsoleFatal prints to stdout using FATAL colors +func ConsoleFatal(s string) { + printConsole(FATAL, s) + os.Exit(1) +} + +// ConsoleFatalf prints a formatted string to stdout using FATAL colors +func ConsoleFatalf(format string, v ...interface{}) { + printfConsole(FATAL, format, v...) + os.Exit(1) +} + func init() { Register("console", NewConsole) } From 90cd10e0348294e74a23eb8058dbb10b63e76bd7 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Sun, 15 Feb 2015 11:26:52 -0700 Subject: [PATCH 043/118] CLI: Add account:delete command --- main.go | 2 +- pkg/cmd/accounts.go | 38 ++++++++++++++++++++++++++++++++ pkg/models/account.go | 4 ++++ pkg/services/sqlstore/account.go | 28 +++++++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 83a83412ee7..c0b7c2fbadc 100644 --- a/main.go +++ b/main.go @@ -32,7 +32,7 @@ func main() { app.Usage = "grafana web" app.Version = version app.Commands = []cli.Command{cmd.CmdWeb, cmd.CmdImportJson, - cmd.CmdListAccounts, cmd.CmdCreateAccount} + cmd.CmdListAccounts, cmd.CmdCreateAccount, cmd.CmdDeleteAccount} app.Flags = append(app.Flags, []cli.Flag{}...) app.Run(os.Args) diff --git a/pkg/cmd/accounts.go b/pkg/cmd/accounts.go index e6742da6fbf..90bff0a8f4d 100644 --- a/pkg/cmd/accounts.go +++ b/pkg/cmd/accounts.go @@ -40,6 +40,20 @@ var CmdCreateAccount = cli.Command{ }, } +var CmdDeleteAccount = cli.Command{ + Name: "account:delete", + Usage: "delete an existing account", + Description: "Deletes an existing account", + Action: deleteAccount, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "config", + Value: "grafana.ini", + Usage: "path to config file", + }, + }, +} + func listAccounts(c *cli.Context) { setting.NewConfigContext() sqlstore.NewEngine() @@ -85,3 +99,27 @@ func createAccount(c *cli.Context) { log.ConsoleInfof("Account %s created for admin user %s\n", name, adminUser.Email) } + +func deleteAccount(c *cli.Context) { + setting.NewConfigContext() + sqlstore.NewEngine() + sqlstore.EnsureAdminUser() + + if !c.Args().Present() { + log.ConsoleFatal("Account name arg is required") + } + + name := c.Args().First() + accountQuery := m.GetAccountByNameQuery{Name: name} + if err := bus.Dispatch(&accountQuery); err != nil { + log.ConsoleFatalf("Failed to find account: %s", err) + } + + accountId := accountQuery.Result.Id + cmd := m.DeleteAccountCommand{Id: accountId} + if err := bus.Dispatch(&cmd); err != nil { + log.ConsoleFatalf("Failed to delete account: %s", err) + } + + log.ConsoleInfof("Account %s deleted", name) +} diff --git a/pkg/models/account.go b/pkg/models/account.go index 12696cc962d..3133a3dfdac 100644 --- a/pkg/models/account.go +++ b/pkg/models/account.go @@ -29,6 +29,10 @@ type CreateAccountCommand struct { Result Account `json:"-"` } +type DeleteAccountCommand struct { + Id int64 +} + type UpdateAccountCommand struct { Name string `json:"name" binding:"Required"` AccountId int64 `json:"-"` diff --git a/pkg/services/sqlstore/account.go b/pkg/services/sqlstore/account.go index 7b0e2bb96b6..7cd24c67c68 100644 --- a/pkg/services/sqlstore/account.go +++ b/pkg/services/sqlstore/account.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/events" + "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" ) @@ -15,6 +16,7 @@ func init() { bus.AddHandler("sql", UpdateAccount) bus.AddHandler("sql", GetAccountByName) bus.AddHandler("sql", GetAccountsQuery) + bus.AddHandler("sql", DeleteAccount) } func GetAccountsQuery(query *m.GetAccountsQuery) error { @@ -106,3 +108,29 @@ func UpdateAccount(cmd *m.UpdateAccountCommand) error { return nil }) } + +func DeleteAccount(cmd *m.DeleteAccountCommand) error { + return inTransaction2(func(sess *session) error { + + deletes := []string{ + "DELETE FROM star WHERE EXISTS (SELECT 1 FROM dashboard WHERE account_id = ?)", + "DELETE FROM dashboard_tag WHERE EXISTS (SELECT 1 FROM dashboard WHERE account_id = ?)", + "DELETE FROM dashboard WHERE account_id = ?", + "DELETE FROM api_key WHERE account_id = ?", + "DELETE FROM data_source WHERE account_id = ?", + "DELETE FROM account_user WHERE account_id = ?", + "DELETE FROM user WHERE account_id = ?", + "DELETE FROM account WHERE id = ?", + } + + for _, sql := range deletes { + log.Trace(sql) + _, err := sess.Exec(sql, cmd.Id) + if err != nil { + return err + } + } + + return nil + }) +} From ca4124940a705a19c8db86e4f053e1df90bc23e7 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Sun, 15 Feb 2015 11:30:35 -0700 Subject: [PATCH 044/118] CLI: Remove redundant Cmd prefix from commands --- main.go | 4 ++-- pkg/cmd/accounts.go | 6 +++--- pkg/cmd/import.go | 2 +- pkg/cmd/web.go | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index c0b7c2fbadc..e5a4c1bc725 100644 --- a/main.go +++ b/main.go @@ -31,8 +31,8 @@ func main() { app.Name = "Grafana Backend" app.Usage = "grafana web" app.Version = version - app.Commands = []cli.Command{cmd.CmdWeb, cmd.CmdImportJson, - cmd.CmdListAccounts, cmd.CmdCreateAccount, cmd.CmdDeleteAccount} + app.Commands = []cli.Command{cmd.Web, cmd.ImportJson, + cmd.ListAccounts, cmd.CreateAccount, cmd.DeleteAccount} app.Flags = append(app.Flags, []cli.Flag{}...) app.Run(os.Args) diff --git a/pkg/cmd/accounts.go b/pkg/cmd/accounts.go index 90bff0a8f4d..cc5de2ae18d 100644 --- a/pkg/cmd/accounts.go +++ b/pkg/cmd/accounts.go @@ -12,7 +12,7 @@ import ( "text/tabwriter" ) -var CmdListAccounts = cli.Command{ +var ListAccounts = cli.Command{ Name: "account", Usage: "list accounts", Description: "Lists the accounts in the system", @@ -26,7 +26,7 @@ var CmdListAccounts = cli.Command{ }, } -var CmdCreateAccount = cli.Command{ +var CreateAccount = cli.Command{ Name: "account:create", Usage: "create a new account", Description: "Creates a new account", @@ -40,7 +40,7 @@ var CmdCreateAccount = cli.Command{ }, } -var CmdDeleteAccount = cli.Command{ +var DeleteAccount = cli.Command{ Name: "account:delete", Usage: "delete an existing account", Description: "Deletes an existing account", diff --git a/pkg/cmd/import.go b/pkg/cmd/import.go index aeea8cb17ed..14818d784a8 100644 --- a/pkg/cmd/import.go +++ b/pkg/cmd/import.go @@ -14,7 +14,7 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -var CmdImportJson = cli.Command{ +var ImportJson = cli.Command{ Name: "import-json", Usage: "grafana import", Description: "Starts Grafana import process", diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index d76e7858302..0b02b2011b4 100644 --- a/pkg/cmd/web.go +++ b/pkg/cmd/web.go @@ -22,7 +22,7 @@ import ( "github.com/grafana/grafana/pkg/social" ) -var CmdWeb = cli.Command{ +var Web = cli.Command{ Name: "web", Usage: "grafana web", Description: "Starts Grafana backend & web server", From dda760b9b58bdebfae0e23c27718b99e63e59662 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Sun, 15 Feb 2015 11:44:17 -0700 Subject: [PATCH 045/118] CLI: Add datasource list command Lists all the datasources for an account via the CLI --- main.go | 3 ++- pkg/cmd/datasource.go | 59 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 pkg/cmd/datasource.go diff --git a/main.go b/main.go index e5a4c1bc725..823a347f747 100644 --- a/main.go +++ b/main.go @@ -32,7 +32,8 @@ func main() { app.Usage = "grafana web" app.Version = version app.Commands = []cli.Command{cmd.Web, cmd.ImportJson, - cmd.ListAccounts, cmd.CreateAccount, cmd.DeleteAccount} + cmd.ListAccounts, cmd.CreateAccount, cmd.DeleteAccount, + cmd.ListDataSources} app.Flags = append(app.Flags, []cli.Flag{}...) app.Run(os.Args) diff --git a/pkg/cmd/datasource.go b/pkg/cmd/datasource.go new file mode 100644 index 00000000000..c4d5f6042e2 --- /dev/null +++ b/pkg/cmd/datasource.go @@ -0,0 +1,59 @@ +package cmd + +import ( + "fmt" + "github.com/codegangsta/cli" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" + "os" + "text/tabwriter" +) + +var ListDataSources = cli.Command{ + Name: "datasource", + Usage: "list datasources", + Description: "Lists the datasources in the system", + Action: listDatasources, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "config", + Value: "grafana.ini", + Usage: "path to config file", + }, + }, +} + +func listDatasources(c *cli.Context) { + setting.NewConfigContext() + sqlstore.NewEngine() + sqlstore.EnsureAdminUser() + + if !c.Args().Present() { + log.ConsoleFatal("Account name arg is required") + } + + name := c.Args().First() + accountQuery := m.GetAccountByNameQuery{Name: name} + if err := bus.Dispatch(&accountQuery); err != nil { + log.ConsoleFatalf("Failed to find account: %s", err) + } + + accountId := accountQuery.Result.Id + + query := m.GetDataSourcesQuery{AccountId: accountId} + if err := bus.Dispatch(&query); err != nil { + log.ConsoleFatalf("Failed to find datasources: %s", err) + } + + w := tabwriter.NewWriter(os.Stdout, 20, 1, 4, ' ', 0) + + fmt.Fprintf(w, "ID\tNAME\tURL\tTYPE\tACCESS\tDEFAULT\n") + for _, ds := range query.Result { + fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\n", ds.Id, ds.Name, ds.Url, ds.Type, + ds.Access, ds.IsDefault) + } + w.Flush() +} From 9cb1170361bf33afa061a9940990fae3d000b406 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Sun, 15 Feb 2015 11:48:39 -0700 Subject: [PATCH 046/118] CLI: Move duplicated config flag to global flag --- main.go | 8 +++++++- pkg/cmd/accounts.go | 21 --------------------- pkg/cmd/datasource.go | 7 ------- pkg/cmd/import.go | 5 ----- pkg/cmd/web.go | 7 ------- 5 files changed, 7 insertions(+), 41 deletions(-) diff --git a/main.go b/main.go index 823a347f747..7b3aa59083d 100644 --- a/main.go +++ b/main.go @@ -34,7 +34,13 @@ func main() { app.Commands = []cli.Command{cmd.Web, cmd.ImportJson, cmd.ListAccounts, cmd.CreateAccount, cmd.DeleteAccount, cmd.ListDataSources} - app.Flags = append(app.Flags, []cli.Flag{}...) + app.Flags = append(app.Flags, []cli.Flag{ + cli.StringFlag{ + Name: "config", + Value: "grafana.ini", + Usage: "path to config file", + }, + }...) app.Run(os.Args) log.Close() diff --git a/pkg/cmd/accounts.go b/pkg/cmd/accounts.go index cc5de2ae18d..7849ef22246 100644 --- a/pkg/cmd/accounts.go +++ b/pkg/cmd/accounts.go @@ -17,13 +17,6 @@ var ListAccounts = cli.Command{ Usage: "list accounts", Description: "Lists the accounts in the system", Action: listAccounts, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "config", - Value: "grafana.ini", - Usage: "path to config file", - }, - }, } var CreateAccount = cli.Command{ @@ -31,13 +24,6 @@ var CreateAccount = cli.Command{ Usage: "create a new account", Description: "Creates a new account", Action: createAccount, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "config", - Value: "grafana.ini", - Usage: "path to config file", - }, - }, } var DeleteAccount = cli.Command{ @@ -45,13 +31,6 @@ var DeleteAccount = cli.Command{ Usage: "delete an existing account", Description: "Deletes an existing account", Action: deleteAccount, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "config", - Value: "grafana.ini", - Usage: "path to config file", - }, - }, } func listAccounts(c *cli.Context) { diff --git a/pkg/cmd/datasource.go b/pkg/cmd/datasource.go index c4d5f6042e2..2bab7785434 100644 --- a/pkg/cmd/datasource.go +++ b/pkg/cmd/datasource.go @@ -17,13 +17,6 @@ var ListDataSources = cli.Command{ Usage: "list datasources", Description: "Lists the datasources in the system", Action: listDatasources, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "config", - Value: "grafana.ini", - Usage: "path to config file", - }, - }, } func listDatasources(c *cli.Context) { diff --git a/pkg/cmd/import.go b/pkg/cmd/import.go index 14818d784a8..87754d9415d 100644 --- a/pkg/cmd/import.go +++ b/pkg/cmd/import.go @@ -28,11 +28,6 @@ var ImportJson = cli.Command{ Name: "account", Usage: "Account name to save dashboards under", }, - cli.StringFlag{ - Name: "config", - Value: "grafana.ini", - Usage: "path to config file", - }, }, } diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index 0b02b2011b4..1207a3b20ef 100644 --- a/pkg/cmd/web.go +++ b/pkg/cmd/web.go @@ -27,13 +27,6 @@ var Web = cli.Command{ Usage: "grafana web", Description: "Starts Grafana backend & web server", Action: runWeb, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "config", - Value: "grafana.ini", - Usage: "path to config file", - }, - }, } func newMacaron() *macaron.Macaron { From f443b7087cc2ce31f7c57b027c6f08da94a734a7 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Sun, 15 Feb 2015 12:10:34 -0700 Subject: [PATCH 047/118] CLI: Add datasource:info command Describes the full datasource details given an account and datasource name. --- main.go | 2 +- pkg/cmd/datasource.go | 63 ++++++++++++++++++++++++++--- pkg/models/datasource.go | 6 +++ pkg/services/sqlstore/datasource.go | 11 +++++ 4 files changed, 75 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 7b3aa59083d..0a71f8681de 100644 --- a/main.go +++ b/main.go @@ -33,7 +33,7 @@ func main() { app.Version = version app.Commands = []cli.Command{cmd.Web, cmd.ImportJson, cmd.ListAccounts, cmd.CreateAccount, cmd.DeleteAccount, - cmd.ListDataSources} + cmd.ListDataSources, cmd.DescribeDataSource} app.Flags = append(app.Flags, []cli.Flag{ cli.StringFlag{ Name: "config", diff --git a/pkg/cmd/datasource.go b/pkg/cmd/datasource.go index 2bab7785434..324b36e53c1 100644 --- a/pkg/cmd/datasource.go +++ b/pkg/cmd/datasource.go @@ -12,12 +12,20 @@ import ( "text/tabwriter" ) -var ListDataSources = cli.Command{ - Name: "datasource", - Usage: "list datasources", - Description: "Lists the datasources in the system", - Action: listDatasources, -} +var ( + ListDataSources = cli.Command{ + Name: "datasource", + Usage: "list datasources", + Description: "Lists the datasources in the system", + Action: listDatasources, + } + DescribeDataSource = cli.Command{ + Name: "datasource:info", + Usage: "describe the details of a datasource", + Description: "Describes the details of a datasource", + Action: describeDataSource, + } +) func listDatasources(c *cli.Context) { setting.NewConfigContext() @@ -50,3 +58,46 @@ func listDatasources(c *cli.Context) { } w.Flush() } + +func describeDataSource(c *cli.Context) { + setting.NewConfigContext() + sqlstore.NewEngine() + sqlstore.EnsureAdminUser() + + if len(c.Args()) != 2 { + log.ConsoleFatal("Account and datasource name args are required") + } + + name := c.Args().First() + ds := c.Args()[1] + + accountQuery := m.GetAccountByNameQuery{Name: name} + if err := bus.Dispatch(&accountQuery); err != nil { + log.ConsoleFatalf("Failed to find account: %s", err) + } + + accountId := accountQuery.Result.Id + + query := m.GetDataSourceByNameQuery{AccountId: accountId, Name: ds} + if err := bus.Dispatch(&query); err != nil { + log.ConsoleFatalf("Failed to find accounts: %s", err) + } + datasource := query.Result + + w := tabwriter.NewWriter(os.Stdout, 20, 1, 4, ' ', 0) + fmt.Fprintf(w, "NAME\t%s\n", datasource.Name) + fmt.Fprintf(w, "URL\t%s\n", datasource.Url) + fmt.Fprintf(w, "DEFAULT\t%t\n", datasource.IsDefault) + fmt.Fprintf(w, "ACCESS\t%s\n", datasource.Access) + fmt.Fprintf(w, "TYPE\t%s\n", datasource.Type) + + switch datasource.Type { + case m.DS_INFLUXDB: + fmt.Fprintf(w, "DATABASE\t%s\n", datasource.Database) + fmt.Fprintf(w, "DB USER\t%s\n", datasource.User) + fmt.Fprintf(w, "DB PASSWORD\t%s\n", datasource.Password) + case m.DS_ES: + fmt.Fprintf(w, "INDEX\t%s\n", datasource.Database) + } + w.Flush() +} diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index cd43f8d66f2..d8aeee27021 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -93,6 +93,12 @@ type GetDataSourceByIdQuery struct { Result DataSource } +type GetDataSourceByNameQuery struct { + Name string + AccountId int64 + Result DataSource +} + // --------------------- // EVENTS type DataSourceCreatedEvent struct { diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go index 842b2bfb1c6..12ea001c559 100644 --- a/pkg/services/sqlstore/datasource.go +++ b/pkg/services/sqlstore/datasource.go @@ -15,6 +15,7 @@ func init() { bus.AddHandler("sql", DeleteDataSource) bus.AddHandler("sql", UpdateDataSource) bus.AddHandler("sql", GetDataSourceById) + bus.AddHandler("sql", GetDataSourceByName) } func GetDataSourceById(query *m.GetDataSourceByIdQuery) error { @@ -27,6 +28,16 @@ func GetDataSourceById(query *m.GetDataSourceByIdQuery) error { return err } +func GetDataSourceByName(query *m.GetDataSourceByNameQuery) error { + sess := x.Limit(100, 0).Where("account_id=? AND name=?", query.AccountId, query.Name) + has, err := sess.Get(&query.Result) + + if !has { + return m.ErrDataSourceNotFound + } + return err +} + func GetDataSources(query *m.GetDataSourcesQuery) error { sess := x.Limit(100, 0).Where("account_id=?", query.AccountId).Asc("name") From 04a970eda2c5301b69236d5fd90e3014e07ab087 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Sun, 15 Feb 2015 12:30:09 -0700 Subject: [PATCH 048/118] CLI: Use 8 char min col size instead of 20 --- pkg/cmd/accounts.go | 2 +- pkg/cmd/datasource.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/accounts.go b/pkg/cmd/accounts.go index 7849ef22246..f2f8d27cacd 100644 --- a/pkg/cmd/accounts.go +++ b/pkg/cmd/accounts.go @@ -43,7 +43,7 @@ func listAccounts(c *cli.Context) { log.ConsoleFatalf("Failed to find accounts: %s", err) } - w := tabwriter.NewWriter(os.Stdout, 20, 1, 4, ' ', 0) + w := tabwriter.NewWriter(os.Stdout, 8, 1, 4, ' ', 0) fmt.Fprintf(w, "ID\tNAME\n") for _, account := range accountsQuery.Result { diff --git a/pkg/cmd/datasource.go b/pkg/cmd/datasource.go index 324b36e53c1..093a635a014 100644 --- a/pkg/cmd/datasource.go +++ b/pkg/cmd/datasource.go @@ -49,7 +49,7 @@ func listDatasources(c *cli.Context) { log.ConsoleFatalf("Failed to find datasources: %s", err) } - w := tabwriter.NewWriter(os.Stdout, 20, 1, 4, ' ', 0) + w := tabwriter.NewWriter(os.Stdout, 8, 1, 4, ' ', 0) fmt.Fprintf(w, "ID\tNAME\tURL\tTYPE\tACCESS\tDEFAULT\n") for _, ds := range query.Result { @@ -84,7 +84,7 @@ func describeDataSource(c *cli.Context) { } datasource := query.Result - w := tabwriter.NewWriter(os.Stdout, 20, 1, 4, ' ', 0) + w := tabwriter.NewWriter(os.Stdout, 8, 1, 4, ' ', 0) fmt.Fprintf(w, "NAME\t%s\n", datasource.Name) fmt.Fprintf(w, "URL\t%s\n", datasource.Url) fmt.Fprintf(w, "DEFAULT\t%t\n", datasource.IsDefault) From 22652889b2c8950aa437f04581aba9afe7535ee9 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Sun, 15 Feb 2015 13:22:20 -0700 Subject: [PATCH 049/118] CLI: Add datasource:create command Allows creating a datasource from the command line --- main.go | 2 +- pkg/cmd/datasource.go | 99 +++++++++++++++++++++++++++++++++++++++- pkg/models/datasource.go | 1 + 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 0a71f8681de..acb48996af9 100644 --- a/main.go +++ b/main.go @@ -33,7 +33,7 @@ func main() { app.Version = version app.Commands = []cli.Command{cmd.Web, cmd.ImportJson, cmd.ListAccounts, cmd.CreateAccount, cmd.DeleteAccount, - cmd.ListDataSources, cmd.DescribeDataSource} + cmd.ListDataSources, cmd.CreateDataSource, cmd.DescribeDataSource} app.Flags = append(app.Flags, []cli.Flag{ cli.StringFlag{ Name: "config", diff --git a/pkg/cmd/datasource.go b/pkg/cmd/datasource.go index 093a635a014..c6691cadd06 100644 --- a/pkg/cmd/datasource.go +++ b/pkg/cmd/datasource.go @@ -19,6 +19,41 @@ var ( Description: "Lists the datasources in the system", Action: listDatasources, } + CreateDataSource = cli.Command{ + Name: "datasource:create", + Usage: "creates a new datasource", + Description: "Creates a new datasource", + Action: createDataSource, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "type", + Value: "graphite", + Usage: fmt.Sprintf("Datasource type [%s,%s,%s,%s]", + m.DS_GRAPHITE, m.DS_INFLUXDB, m.DS_ES, m.DS_OPENTSDB), + }, + cli.StringFlag{ + Name: "access", + Value: "proxy", + Usage: "Datasource access [proxy,direct]", + }, + cli.BoolFlag{ + Name: "default", + Usage: "Make this the default datasource", + }, + cli.StringFlag{ + Name: "db", + Usage: "InfluxDB DB", + }, + cli.StringFlag{ + Name: "user", + Usage: "InfluxDB username", + }, + cli.StringFlag{ + Name: "password", + Usage: "InfluxDB password", + }, + }, + } DescribeDataSource = cli.Command{ Name: "datasource:info", Usage: "describe the details of a datasource", @@ -27,6 +62,68 @@ var ( } ) +func createDataSource(c *cli.Context) { + setting.NewConfigContext() + sqlstore.NewEngine() + sqlstore.EnsureAdminUser() + + if len(c.Args()) != 3 { + log.ConsoleFatal("Missing required arguments") + } + + name := c.Args().First() + ds := c.Args()[1] + url := c.Args()[2] + dsType := c.String("type") + dsAccess := c.String("access") + dsDefault := c.Bool("default") + + accountQuery := m.GetAccountByNameQuery{Name: name} + if err := bus.Dispatch(&accountQuery); err != nil { + log.ConsoleFatalf("Failed to find account: %s", err) + } + + accountId := accountQuery.Result.Id + + query := m.GetDataSourceByNameQuery{AccountId: accountId, Name: ds} + if err := bus.Dispatch(&query); err != nil { + if err != m.ErrDataSourceNotFound { + log.ConsoleFatalf("Failed to query for existing datasource: %s", err) + } + } + + if query.Result.Id > 0 { + log.ConsoleFatalf("DataSource %s already exists", ds) + } + + cmd := m.AddDataSourceCommand{ + AccountId: accountId, + Name: ds, + Url: url, + Type: m.DsType(dsType), + Access: m.DsAccess(dsAccess), + IsDefault: dsDefault, + } + + switch dsType { + case m.DS_INFLUXDB: + db := c.String("db") + if db == "" { + log.ConsoleFatal("db name is required for influxdb datasources") + } + cmd.Database = db + cmd.User = c.String("user") + cmd.Password = c.String("password") + } + + if err := bus.Dispatch(&cmd); err != nil { + log.ConsoleFatalf("Failed to create datasource: %s", err) + } + datasource := cmd.Result + + log.ConsoleInfof("Datasource %s created", datasource.Name) +} + func listDatasources(c *cli.Context) { setting.NewConfigContext() sqlstore.NewEngine() @@ -80,7 +177,7 @@ func describeDataSource(c *cli.Context) { query := m.GetDataSourceByNameQuery{AccountId: accountId, Name: ds} if err := bus.Dispatch(&query); err != nil { - log.ConsoleFatalf("Failed to find accounts: %s", err) + log.ConsoleFatalf("Failed to find datasource: %s", err) } datasource := query.Result diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index d8aeee27021..8f1eb730110 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -9,6 +9,7 @@ const ( DS_GRAPHITE = "graphite" DS_INFLUXDB = "influxdb" DS_ES = "elasticsearch" + DS_OPENTSDB = "opentsdb" DS_ACCESS_DIRECT = "direct" DS_ACCESS_PROXY = "proxy" ) From b97361b1938d48218ed91a3cd11c574027dddba7 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Sun, 15 Feb 2015 13:31:00 -0700 Subject: [PATCH 050/118] CLI: Add datasource:delete command --- main.go | 2 +- pkg/cmd/datasource.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index acb48996af9..aab96c74bad 100644 --- a/main.go +++ b/main.go @@ -33,7 +33,7 @@ func main() { app.Version = version app.Commands = []cli.Command{cmd.Web, cmd.ImportJson, cmd.ListAccounts, cmd.CreateAccount, cmd.DeleteAccount, - cmd.ListDataSources, cmd.CreateDataSource, cmd.DescribeDataSource} + cmd.ListDataSources, cmd.CreateDataSource, cmd.DescribeDataSource, cmd.DeleteDataSource} app.Flags = append(app.Flags, []cli.Flag{ cli.StringFlag{ Name: "config", diff --git a/pkg/cmd/datasource.go b/pkg/cmd/datasource.go index c6691cadd06..7e292579227 100644 --- a/pkg/cmd/datasource.go +++ b/pkg/cmd/datasource.go @@ -60,6 +60,12 @@ var ( Description: "Describes the details of a datasource", Action: describeDataSource, } + DeleteDataSource = cli.Command{ + Name: "datasource:delete", + Usage: "Deletes a datasource", + Description: "Deletes a datasource", + Action: deleteDataSource, + } ) func createDataSource(c *cli.Context) { @@ -198,3 +204,36 @@ func describeDataSource(c *cli.Context) { } w.Flush() } + +func deleteDataSource(c *cli.Context) { + setting.NewConfigContext() + sqlstore.NewEngine() + sqlstore.EnsureAdminUser() + + if len(c.Args()) != 2 { + log.ConsoleFatal("Account and datasource name args are required") + } + + name := c.Args().First() + ds := c.Args()[1] + + accountQuery := m.GetAccountByNameQuery{Name: name} + if err := bus.Dispatch(&accountQuery); err != nil { + log.ConsoleFatalf("Failed to find account: %s", err) + } + + accountId := accountQuery.Result.Id + + query := m.GetDataSourceByNameQuery{AccountId: accountId, Name: ds} + if err := bus.Dispatch(&query); err != nil { + log.ConsoleFatalf("Failed to find datasource: %s", err) + } + datasource := query.Result + + cmd := m.DeleteDataSourceCommand{AccountId: accountId, Id: datasource.Id} + if err := bus.Dispatch(&cmd); err != nil { + log.ConsoleFatalf("Failed to delete datasource: %s", err) + } + + log.ConsoleInfof("DataSource %s deleted", ds) +} From f2e9ec63dfac73f10a6f30103f12e1d7b4ccec77 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Sun, 15 Feb 2015 13:45:25 -0700 Subject: [PATCH 051/118] CLI: Renamed import-json to dashboard:import More consistent w/ other command names. --- pkg/cmd/{import.go => dashboard.go} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename pkg/cmd/{import.go => dashboard.go} (96%) diff --git a/pkg/cmd/import.go b/pkg/cmd/dashboard.go similarity index 96% rename from pkg/cmd/import.go rename to pkg/cmd/dashboard.go index 87754d9415d..f1f7f9fedb2 100644 --- a/pkg/cmd/import.go +++ b/pkg/cmd/dashboard.go @@ -15,8 +15,8 @@ import ( ) var ImportJson = cli.Command{ - Name: "import-json", - Usage: "grafana import", + Name: "dashboard:import", + Usage: "imports dashboards in JSON from a directory", Description: "Starts Grafana import process", Action: runImport, Flags: []cli.Flag{ From 7c8fa067a2691f8fe4f7ec3cd5511c7a941f70f3 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Sun, 15 Feb 2015 13:48:36 -0700 Subject: [PATCH 052/118] CLI: Conver account flag to required arg Follows the same convention as other commands. --- pkg/cmd/dashboard.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/dashboard.go b/pkg/cmd/dashboard.go index f1f7f9fedb2..b37e06f4b8b 100644 --- a/pkg/cmd/dashboard.go +++ b/pkg/cmd/dashboard.go @@ -24,10 +24,6 @@ var ImportJson = cli.Command{ Name: "dir", Usage: "path to folder containing json dashboards", }, - cli.StringFlag{ - Name: "account", - Usage: "Account name to save dashboards under", - }, }, } @@ -49,12 +45,12 @@ func runImport(c *cli.Context) { return } - accountName := c.String("account") - if len(accountName) == 0 { - log.Error(3, "Missing command flag --account") - return + if !c.Args().Present() { + log.ConsoleFatal("Account name arg is required") } + accountName := c.Args().First() + setting.NewConfigContext() sqlstore.NewEngine() sqlstore.EnsureAdminUser() From a3925e8aa0510ada4c5f580bc41341ba96dbcbcd Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Sun, 15 Feb 2015 13:51:41 -0700 Subject: [PATCH 053/118] CLI: Use console logger for dashbard:import command More consistent w/ other commands and separates stdout/stderr --- pkg/cmd/dashboard.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/pkg/cmd/dashboard.go b/pkg/cmd/dashboard.go index b37e06f4b8b..638c80cf38c 100644 --- a/pkg/cmd/dashboard.go +++ b/pkg/cmd/dashboard.go @@ -30,19 +30,16 @@ var ImportJson = cli.Command{ func runImport(c *cli.Context) { dir := c.String("dir") if len(dir) == 0 { - log.Error(3, "Missing command flag --dir") - return + log.ConsoleFatalf("Missing command flag --dir") } file, err := os.Stat(dir) if os.IsNotExist(err) { - log.Error(3, "Directory does not exist: %v", dir) - return + log.ConsoleFatalf("Directory does not exist: %v", dir) } if !file.IsDir() { - log.Error(3, "%v is not a directory", dir) - return + log.ConsoleFatalf("%v is not a directory", dir) } if !c.Args().Present() { @@ -57,8 +54,7 @@ func runImport(c *cli.Context) { accountQuery := m.GetAccountByNameQuery{Name: accountName} if err := bus.Dispatch(&accountQuery); err != nil { - log.Error(3, "Failed to find account", err) - return + log.ConsoleFatalf("Failed to find account", err) } accountId := accountQuery.Result.Id @@ -72,19 +68,19 @@ func runImport(c *cli.Context) { } if strings.HasSuffix(f.Name(), ".json") { if err := importDashboard(path, accountId); err != nil { - log.Error(3, "Failed to import dashboard file: %v, err: %v", path, err) + log.ConsoleFatalf("Failed to import dashboard file: %v, err: %v", path, err) } } return nil } if err := filepath.Walk(dir, visitor); err != nil { - log.Error(3, "failed to scan dir for json files: %v", err) + log.ConsoleFatalf("Failed to scan dir for json files: %v", err) } } func importDashboard(path string, accountId int64) error { - log.Info("Importing %v", path) + log.ConsoleInfof("Importing %v", path) reader, err := os.Open(path) if err != nil { From 9223c954818f722cfab908356715b31bb2137916 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Sun, 15 Feb 2015 14:00:06 -0700 Subject: [PATCH 054/118] CLI: Order commands alphabetically --- main.go | 6 ++++-- pkg/cmd/dashboard.go | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index aab96c74bad..c48707fa6e5 100644 --- a/main.go +++ b/main.go @@ -31,9 +31,11 @@ func main() { app.Name = "Grafana Backend" app.Usage = "grafana web" app.Version = version - app.Commands = []cli.Command{cmd.Web, cmd.ImportJson, + app.Commands = []cli.Command{ cmd.ListAccounts, cmd.CreateAccount, cmd.DeleteAccount, - cmd.ListDataSources, cmd.CreateDataSource, cmd.DescribeDataSource, cmd.DeleteDataSource} + cmd.ImportDashboard, + cmd.ListDataSources, cmd.CreateDataSource, cmd.DescribeDataSource, cmd.DeleteDataSource, + cmd.Web} app.Flags = append(app.Flags, []cli.Flag{ cli.StringFlag{ Name: "config", diff --git a/pkg/cmd/dashboard.go b/pkg/cmd/dashboard.go index 638c80cf38c..a805e16eccb 100644 --- a/pkg/cmd/dashboard.go +++ b/pkg/cmd/dashboard.go @@ -14,7 +14,7 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -var ImportJson = cli.Command{ +var ImportDashboard = cli.Command{ Name: "dashboard:import", Usage: "imports dashboards in JSON from a directory", Description: "Starts Grafana import process", From b6428b08d080073d9876524d499d362feb8419b7 Mon Sep 17 00:00:00 2001 From: Jason Wilder Date: Sun, 15 Feb 2015 14:57:49 -0700 Subject: [PATCH 055/118] CLI: Fix config flag being ignored Passing --config had no effect when passed. It will now be applied as the last config file and before any env var overrrides. --- main.go | 3 +-- pkg/cmd/accounts.go | 13 +++---------- pkg/cmd/common.go | 13 +++++++++++++ pkg/cmd/dashboard.go | 6 +----- pkg/cmd/datasource.go | 18 ++++-------------- pkg/cmd/web.go | 6 ++---- pkg/setting/setting.go | 6 +++++- pkg/setting/setting_test.go | 4 ++-- 8 files changed, 31 insertions(+), 38 deletions(-) create mode 100644 pkg/cmd/common.go diff --git a/main.go b/main.go index c48707fa6e5..287ba9aa348 100644 --- a/main.go +++ b/main.go @@ -39,8 +39,7 @@ func main() { app.Flags = append(app.Flags, []cli.Flag{ cli.StringFlag{ Name: "config", - Value: "grafana.ini", - Usage: "path to config file", + Usage: "path to grafana.ini config file", }, }...) app.Run(os.Args) diff --git a/pkg/cmd/accounts.go b/pkg/cmd/accounts.go index f2f8d27cacd..8758cb06e11 100644 --- a/pkg/cmd/accounts.go +++ b/pkg/cmd/accounts.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "os" "text/tabwriter" @@ -34,9 +33,7 @@ var DeleteAccount = cli.Command{ } func listAccounts(c *cli.Context) { - setting.NewConfigContext() - sqlstore.NewEngine() - sqlstore.EnsureAdminUser() + initRuntime(c) accountsQuery := m.GetAccountsQuery{} if err := bus.Dispatch(&accountsQuery); err != nil { @@ -53,9 +50,7 @@ func listAccounts(c *cli.Context) { } func createAccount(c *cli.Context) { - setting.NewConfigContext() - sqlstore.NewEngine() - sqlstore.EnsureAdminUser() + initRuntime(c) if !c.Args().Present() { log.ConsoleFatal("Account name arg is required") @@ -80,9 +75,7 @@ func createAccount(c *cli.Context) { } func deleteAccount(c *cli.Context) { - setting.NewConfigContext() - sqlstore.NewEngine() - sqlstore.EnsureAdminUser() + initRuntime(c) if !c.Args().Present() { log.ConsoleFatal("Account name arg is required") diff --git a/pkg/cmd/common.go b/pkg/cmd/common.go new file mode 100644 index 00000000000..338baa36aac --- /dev/null +++ b/pkg/cmd/common.go @@ -0,0 +1,13 @@ +package cmd + +import ( + "github.com/codegangsta/cli" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" +) + +func initRuntime(c *cli.Context) { + setting.NewConfigContext(c.GlobalString("config")) + sqlstore.NewEngine() + sqlstore.EnsureAdminUser() +} diff --git a/pkg/cmd/dashboard.go b/pkg/cmd/dashboard.go index a805e16eccb..eaae9700164 100644 --- a/pkg/cmd/dashboard.go +++ b/pkg/cmd/dashboard.go @@ -10,8 +10,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/setting" ) var ImportDashboard = cli.Command{ @@ -48,9 +46,7 @@ func runImport(c *cli.Context) { accountName := c.Args().First() - setting.NewConfigContext() - sqlstore.NewEngine() - sqlstore.EnsureAdminUser() + initRuntime(c) accountQuery := m.GetAccountByNameQuery{Name: accountName} if err := bus.Dispatch(&accountQuery); err != nil { diff --git a/pkg/cmd/datasource.go b/pkg/cmd/datasource.go index 7e292579227..bdd458e2b5d 100644 --- a/pkg/cmd/datasource.go +++ b/pkg/cmd/datasource.go @@ -6,8 +6,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/setting" "os" "text/tabwriter" ) @@ -69,9 +67,7 @@ var ( ) func createDataSource(c *cli.Context) { - setting.NewConfigContext() - sqlstore.NewEngine() - sqlstore.EnsureAdminUser() + initRuntime(c) if len(c.Args()) != 3 { log.ConsoleFatal("Missing required arguments") @@ -131,9 +127,7 @@ func createDataSource(c *cli.Context) { } func listDatasources(c *cli.Context) { - setting.NewConfigContext() - sqlstore.NewEngine() - sqlstore.EnsureAdminUser() + initRuntime(c) if !c.Args().Present() { log.ConsoleFatal("Account name arg is required") @@ -163,9 +157,7 @@ func listDatasources(c *cli.Context) { } func describeDataSource(c *cli.Context) { - setting.NewConfigContext() - sqlstore.NewEngine() - sqlstore.EnsureAdminUser() + initRuntime(c) if len(c.Args()) != 2 { log.ConsoleFatal("Account and datasource name args are required") @@ -206,9 +198,7 @@ func describeDataSource(c *cli.Context) { } func deleteDataSource(c *cli.Context) { - setting.NewConfigContext() - sqlstore.NewEngine() - sqlstore.EnsureAdminUser() + initRuntime(c) if len(c.Args()) != 2 { log.ConsoleFatal("Account and datasource name args are required") diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index 1207a3b20ef..b858072398c 100644 --- a/pkg/cmd/web.go +++ b/pkg/cmd/web.go @@ -17,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/services/eventpublisher" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/social" ) @@ -71,10 +70,9 @@ func runWeb(c *cli.Context) { log.Info("Starting Grafana") log.Info("Version: %v, Commit: %v, Build date: %v", setting.BuildVersion, setting.BuildCommit, time.Unix(setting.BuildStamp, 0)) - setting.NewConfigContext() + initRuntime(c) + social.NewOAuthService() - sqlstore.NewEngine() - sqlstore.EnsureAdminUser() eventpublisher.Init() var err error diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 4d3ef9161c3..01e368abc6f 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -164,9 +164,13 @@ func loadEnvVariableOverrides() { } } -func NewConfigContext() { +func NewConfigContext(config string) { configFiles := findConfigFiles() + if config != "" { + configFiles = append(configFiles, config) + } + //log.Info("Loading config files: %v", configFiles) var err error diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index be031e3f4cc..253eb94f123 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -15,7 +15,7 @@ func TestLoadingSettings(t *testing.T) { Convey("Testing loading settings from ini file", t, func() { Convey("Given the default ini files", func() { - NewConfigContext() + NewConfigContext("") So(AppName, ShouldEqual, "Grafana") So(AdminUser, ShouldEqual, "admin") @@ -23,7 +23,7 @@ func TestLoadingSettings(t *testing.T) { Convey("Should be able to override via environment variables", func() { os.Setenv("GF_SECURITY_ADMIN_USER", "superduper") - NewConfigContext() + NewConfigContext("") So(AdminUser, ShouldEqual, "superduper") }) From 56d8fe4a22b0f20da94036d80b531e1ec88d9aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 16 Feb 2015 07:40:21 +0100 Subject: [PATCH 056/118] Small changes to CLI commands PR --- main.go | 9 +++++++-- pkg/cmd/accounts.go | 12 +++++++----- pkg/cmd/dashboard.go | 2 +- pkg/cmd/datasource.go | 9 +++++---- pkg/services/sqlstore/account.go | 1 - pkg/setting/setting.go | 1 - 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/main.go b/main.go index 287ba9aa348..6701a5d7c08 100644 --- a/main.go +++ b/main.go @@ -32,9 +32,14 @@ func main() { app.Usage = "grafana web" app.Version = version app.Commands = []cli.Command{ - cmd.ListAccounts, cmd.CreateAccount, cmd.DeleteAccount, + cmd.ListAccounts, + cmd.CreateAccount, + cmd.DeleteAccount, cmd.ImportDashboard, - cmd.ListDataSources, cmd.CreateDataSource, cmd.DescribeDataSource, cmd.DeleteDataSource, + cmd.ListDataSources, + cmd.CreateDataSource, + cmd.DescribeDataSource, + cmd.DeleteDataSource, cmd.Web} app.Flags = append(app.Flags, []cli.Flag{ cli.StringFlag{ diff --git a/pkg/cmd/accounts.go b/pkg/cmd/accounts.go index 8758cb06e11..aeda7efed9b 100644 --- a/pkg/cmd/accounts.go +++ b/pkg/cmd/accounts.go @@ -2,31 +2,33 @@ package cmd import ( "fmt" + "os" + "text/tabwriter" + "github.com/codegangsta/cli" + "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" - "os" - "text/tabwriter" ) var ListAccounts = cli.Command{ - Name: "account", + Name: "accounts", Usage: "list accounts", Description: "Lists the accounts in the system", Action: listAccounts, } var CreateAccount = cli.Command{ - Name: "account:create", + Name: "accounts:create", Usage: "create a new account", Description: "Creates a new account", Action: createAccount, } var DeleteAccount = cli.Command{ - Name: "account:delete", + Name: "accounts:delete", Usage: "delete an existing account", Description: "Deletes an existing account", Action: deleteAccount, diff --git a/pkg/cmd/dashboard.go b/pkg/cmd/dashboard.go index eaae9700164..43714eb8e93 100644 --- a/pkg/cmd/dashboard.go +++ b/pkg/cmd/dashboard.go @@ -13,7 +13,7 @@ import ( ) var ImportDashboard = cli.Command{ - Name: "dashboard:import", + Name: "dashboards:import", Usage: "imports dashboards in JSON from a directory", Description: "Starts Grafana import process", Action: runImport, diff --git a/pkg/cmd/datasource.go b/pkg/cmd/datasource.go index bdd458e2b5d..660e59a9702 100644 --- a/pkg/cmd/datasource.go +++ b/pkg/cmd/datasource.go @@ -2,23 +2,24 @@ package cmd import ( "fmt" + "os" + "text/tabwriter" + "github.com/codegangsta/cli" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" - "os" - "text/tabwriter" ) var ( ListDataSources = cli.Command{ - Name: "datasource", + Name: "datasources", Usage: "list datasources", Description: "Lists the datasources in the system", Action: listDatasources, } CreateDataSource = cli.Command{ - Name: "datasource:create", + Name: "datasources:create", Usage: "creates a new datasource", Description: "Creates a new datasource", Action: createDataSource, diff --git a/pkg/services/sqlstore/account.go b/pkg/services/sqlstore/account.go index 7cd24c67c68..7eb6c79fae5 100644 --- a/pkg/services/sqlstore/account.go +++ b/pkg/services/sqlstore/account.go @@ -119,7 +119,6 @@ func DeleteAccount(cmd *m.DeleteAccountCommand) error { "DELETE FROM api_key WHERE account_id = ?", "DELETE FROM data_source WHERE account_id = ?", "DELETE FROM account_user WHERE account_id = ?", - "DELETE FROM user WHERE account_id = ?", "DELETE FROM account WHERE id = ?", } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 01e368abc6f..241f820ca6b 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -171,7 +171,6 @@ func NewConfigContext(config string) { configFiles = append(configFiles, config) } - //log.Info("Loading config files: %v", configFiles) var err error for i, file := range configFiles { From 8828db4fcefb8d23e52648f2477bb09c8467ddbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 16 Feb 2015 07:46:47 +0100 Subject: [PATCH 057/118] Fixed issue with date range form validation, Closes #1485 --- src/app/panels/timepicker/custom.html | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/app/panels/timepicker/custom.html b/src/app/panels/timepicker/custom.html index d6eba698b33..c5532ddfb5d 100644 --- a/src/app/panels/timepicker/custom.html +++ b/src/app/panels/timepicker/custom.html @@ -44,7 +44,7 @@
    -
    +
    @@ -73,12 +73,9 @@
    +
    + + Invalid date or range -
    - -
    - Invalid date or range - -
    From 7f5f6763ea0c28715a598f6124354cd463202d78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 16 Feb 2015 07:52:16 +0100 Subject: [PATCH 058/118] Fixed icon for ascending sort order in table legend, #1483 --- src/app/panels/graph/legend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/panels/graph/legend.js b/src/app/panels/graph/legend.js index 2c54d8e9456..d4a48e2f498 100644 --- a/src/app/panels/graph/legend.js +++ b/src/app/panels/graph/legend.js @@ -78,7 +78,7 @@ function (angular, app, _, kbn, $) { var html = '' + statName; if (panel.legend.sort === statName) { - var cssClass = panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa-caret-up' ; + var cssClass = panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up' ; html += ' '; } From 83d798dbad046af968a878175e8451d55a2029af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 16 Feb 2015 08:12:07 +0100 Subject: [PATCH 059/118] Small update to grafana.ini describe server options --- conf/grafana.ini | 10 ++++++++-- pkg/setting/setting.go | 5 ----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/conf/grafana.ini b/conf/grafana.ini index 2d643191b4a..491dfd1063b 100644 --- a/conf/grafana.ini +++ b/conf/grafana.ini @@ -2,12 +2,18 @@ app_name = Grafana app_mode = production [server] +; protocol (http or https) protocol = http -domain = localhost -root_url = %(protocol)s://%(domain)s:%(http_port)s/ +; the ip address to bind to, empty will bind to all interfaces http_addr = +; the http port to use http_port = 3000 +; The public facing domain name used to access grafana from a browser +domain = localhost +; the full public facing url +root_url = %(protocol)s://%(domain)s:%(http_port)s/ router_logging = false +; the path relative to the binary where the static (html/js/css) files are placed static_root_path = public enable_gzip = false diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 241f820ca6b..0212d719e5b 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -204,11 +204,6 @@ func NewConfigContext(config string) { HttpAddr = server.Key("http_addr").MustString("0.0.0.0") HttpPort = server.Key("http_port").MustString("3000") - port := os.Getenv("PORT") - if port != "" { - HttpPort = port - } - StaticRootPath = server.Key("static_root_path").MustString(path.Join(WorkDir, "webapp")) RouterLogging = server.Key("router_logging").MustBool(false) EnableGzip = server.Key("enable_gzip").MustBool(false) From ea8307f79ea01b2a877d019c40c6b47edce00018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 17 Feb 2015 11:44:58 +0100 Subject: [PATCH 060/118] Added influxdb docker block, and fixed issue when adding data source --- docker/blocks/influxdb/Dockerfile | 16 ++++ docker/blocks/influxdb/config.toml | 75 +++++++++++++++++++ docker/blocks/influxdb/fig | 6 ++ docker/fig.yml | 21 ++---- .../features/account/datasourceEditCtrl.js | 1 - 5 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 docker/blocks/influxdb/Dockerfile create mode 100644 docker/blocks/influxdb/config.toml create mode 100644 docker/blocks/influxdb/fig diff --git a/docker/blocks/influxdb/Dockerfile b/docker/blocks/influxdb/Dockerfile new file mode 100644 index 00000000000..69d10992464 --- /dev/null +++ b/docker/blocks/influxdb/Dockerfile @@ -0,0 +1,16 @@ +# influxdb + +FROM ubuntu + +RUN mkdir -p /opt/influxdb/shared/data + +ADD http://s3.amazonaws.com/influxdb/influxdb_0.8.8_amd64.deb /influx88.deb +RUN dpkg -i /influx88.deb +RUN rm -rf /opt/influxdb/shared/data + +ADD config.toml /opt/influxdb/shared/config.toml + +EXPOSE 8083 8086 2004 + +ENTRYPOINT ["/usr/bin/influxdb"] +CMD ["-config=/opt/influxdb/shared/config.toml"] diff --git a/docker/blocks/influxdb/config.toml b/docker/blocks/influxdb/config.toml new file mode 100644 index 00000000000..23834b22e3d --- /dev/null +++ b/docker/blocks/influxdb/config.toml @@ -0,0 +1,75 @@ +bind-address = "0.0.0.0" + +[logging] +level = "debug" +file = "/opt/influxdb/shared/data/influxdb.log" # stdout to log to standard out + +[admin] +port = 8083 # binding is disabled if the port isn't set +assets = "/opt/influxdb/current/admin" + +[api] +port = 8086 # binding is disabled if the port isn't set + +read-timeout = "5s" + +[input_plugins] + + [input_plugins.graphite] + enabled = true + port = 2004 + database = "graphite" # store graphite data in this database + + +[raft] +port = 8090 +dir = "/opt/influxdb/shared/data/raft" + +[storage] +dir = "/opt/influxdb/shared/data/db" +# How many requests to potentially buffer in memory. If the buffer gets filled then writes +# will still be logged and once the local storage has caught up (or compacted) the writes +# will be replayed from the WAL +write-buffer-size = 10000 +default-engine = "rocksdb" +max-open-shards = 0 +point-batch-size = 100 +write-batch-size = 5000000 +retention-sweep-period = "10m" + +[storage.engines.rocksdb] +max-open-files = 1000 +lru-cache-size = "200m" + +[storage.engines.leveldb] +max-open-files = 1000 +lru-cache-size = "200m" + +[cluster] +protobuf_port = 8099 +protobuf_timeout = "2s" # the write timeout on the protobuf conn any duration parseable by time.ParseDuration +protobuf_heartbeat = "200ms" # the heartbeat interval between the servers. must be parseable by time.ParseDuration +protobuf_min_backoff = "1s" # the minimum backoff after a failed heartbeat attempt +protobuf_max_backoff = "10s" # the maxmimum backoff after a failed heartbeat attempt +write-buffer-size = 10000 +ax-response-buffer-size = 100000 +oncurrent-shard-query-limit = 10 + +[sharding] + replication-factor = 1 + + [sharding.short-term] + duration = "7d" + split = 1 + + [sharding.long-term] + duration = "30d" + split = 1 + # split-random = "/^Hf.*/" + +[wal] +dir = "/opt/influxdb/shared/data/wal" +flush-after = 1000 # the number of writes after which wal will be flushed, 0 for flushing on every write +bookmark-after = 1000 # the number of writes after which a bookmark will be created +index-after = 1000 +requests-per-logfile = 10000 diff --git a/docker/blocks/influxdb/fig b/docker/blocks/influxdb/fig new file mode 100644 index 00000000000..3f247f756e2 --- /dev/null +++ b/docker/blocks/influxdb/fig @@ -0,0 +1,6 @@ +influxdb: + build: blocks/influxdb + ports: + - "2004:2004" + - "8083:8083" + - "8086:8086" diff --git a/docker/fig.yml b/docker/fig.yml index fa47a0ce914..81a6e9c6fc8 100644 --- a/docker/fig.yml +++ b/docker/fig.yml @@ -1,18 +1,7 @@ -mysqltests: - image: mysql:latest - environment: - MYSQL_ROOT_PASSWORD: rootpass - MYSQL_DATABASE: grafana_tests - MYSQL_USER: grafana - MYSQL_PASSWORD: password +influxdb: + build: blocks/influxdb ports: - - "3306:3306" - -postgrestest: - image: postgres:latest - environment: - POSTGRES_USER: grafanatest - POSTGRES_PASSWORD: grafanatest - ports: - - "5432:5432" + - "2004:2004" + - "8083:8083" + - "8086:8086" diff --git a/src/app/features/account/datasourceEditCtrl.js b/src/app/features/account/datasourceEditCtrl.js index bf1912f2677..389bf8bc7a8 100644 --- a/src/app/features/account/datasourceEditCtrl.js +++ b/src/app/features/account/datasourceEditCtrl.js @@ -58,7 +58,6 @@ function (angular) { backendSrv.put('/api/datasources', $scope.current) .then(function() { - $scope.editor.index = 0; $scope.getDatasources(); }); }; From 946afccbb5d44b507973ee3633abadad2548240d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 17 Feb 2015 18:20:47 +0100 Subject: [PATCH 061/118] Fixed dashboard search dropdown issue where it closes dashboard settings view if open, also fixed so that clicking outside dashboard search will close it, Fixes #1489 --- src/app/controllers/search.js | 8 +-- src/app/directives/dashEditLink.js | 1 - src/app/features/dashboard/all.js | 1 + .../features/dashboard/dashboardNavCtrl.js | 2 +- .../dashboard/directives/dashSearchView.js | 62 +++++++++++++++++++ src/app/features/dashboard/keybindings.js | 2 +- src/app/partials/dashboard.html | 4 +- src/app/partials/search.html | 4 +- src/css/less/bootswatch.dark.less | 4 +- src/css/less/search.less | 4 +- 10 files changed, 76 insertions(+), 16 deletions(-) create mode 100644 src/app/features/dashboard/directives/dashSearchView.js diff --git a/src/app/controllers/search.js b/src/app/controllers/search.js index 6aa525b7f91..3819be3a2e7 100644 --- a/src/app/controllers/search.js +++ b/src/app/controllers/search.js @@ -2,9 +2,8 @@ define([ 'angular', 'lodash', 'config', - 'jquery' ], -function (angular, _, config, $) { +function (angular, _, config) { 'use strict'; var module = angular.module('grafana.controllers'); @@ -29,7 +28,7 @@ function (angular, _, config, $) { $scope.keyDown = function (evt) { if (evt.keyCode === 27) { - $scope.appEvent('hide-dash-editor'); + $scope.dismiss(); } if (evt.keyCode === 40) { $scope.moveSelection(1); @@ -50,9 +49,6 @@ function (angular, _, config, $) { if (selectedDash) { $location.search({}); $location.path("/dashboard/db/" + selectedDash.slug); - setTimeout(function() { - $('body').click(); // hack to force dropdown to close; - }); } } }; diff --git a/src/app/directives/dashEditLink.js b/src/app/directives/dashEditLink.js index 0933d3f96f2..1b5173671b0 100644 --- a/src/app/directives/dashEditLink.js +++ b/src/app/directives/dashEditLink.js @@ -118,5 +118,4 @@ function (angular, $) { } }; }); - }); diff --git a/src/app/features/dashboard/all.js b/src/app/features/dashboard/all.js index 8433ecc54ce..1ba0051adf3 100644 --- a/src/app/features/dashboard/all.js +++ b/src/app/features/dashboard/all.js @@ -11,4 +11,5 @@ define([ './playlistSrv', './timeSrv', './unsavedChangesSrv', + './directives/dashSearchView', ], function () {}); diff --git a/src/app/features/dashboard/dashboardNavCtrl.js b/src/app/features/dashboard/dashboardNavCtrl.js index 4955c2e5185..b256b24d6cf 100644 --- a/src/app/features/dashboard/dashboardNavCtrl.js +++ b/src/app/features/dashboard/dashboardNavCtrl.js @@ -80,7 +80,7 @@ function (angular, _, moment, config, store) { }; $scope.openSearch = function() { - $scope.appEvent('show-dash-editor', { src: 'app/partials/search.html', cssClass: 'search-container' }); + $scope.appEvent('show-dash-search'); }; $scope.saveDashboard = function() { diff --git a/src/app/features/dashboard/directives/dashSearchView.js b/src/app/features/dashboard/directives/dashSearchView.js new file mode 100644 index 00000000000..ad73b84043c --- /dev/null +++ b/src/app/features/dashboard/directives/dashSearchView.js @@ -0,0 +1,62 @@ +define([ + 'angular', + 'jquery' +], +function (angular, $) { + 'use strict'; + + angular + .module('grafana.directives') + .directive('dashSearchView', function($compile, $timeout) { + return { + restrict: 'A', + link: function(scope, elem) { + var editorScope; + + function hookUpHideWhenClickedOutside() { + $timeout(function() { + $(document).bind('click.hide-search', function(evt) { + // some items can be inside container + // but then removed + if ($(evt.target).parents().length === 0) { + return; + } + + if ($(evt.target).parents('.search-container').length === 0) { + if (editorScope) { + editorScope.dismiss(); + } + } + }); + }); + } + + function showSearch() { + if (editorScope) { + editorScope.dismiss(); + return; + } + + editorScope = scope.$new(); + editorScope.dismiss = function() { + editorScope.$destroy(); + elem.empty(); + elem.unbind(); + editorScope = null; + $(document).unbind('click.hide-search'); + }; + + var view = $('
    '); + + elem.append(view); + $compile(elem.contents())(editorScope); + + hookUpHideWhenClickedOutside(); + } + + scope.onAppEvent('show-dash-search', showSearch); + } + }; + }); + +}); diff --git a/src/app/features/dashboard/keybindings.js b/src/app/features/dashboard/keybindings.js index c0cddc5b493..15855c80bf4 100644 --- a/src/app/features/dashboard/keybindings.js +++ b/src/app/features/dashboard/keybindings.js @@ -40,7 +40,7 @@ function(angular, $) { }, { inputDisabled: true }); keyboardManager.bind('ctrl+f', function() { - scope.appEvent('show-dash-editor', { src: 'app/partials/search.html', cssClass: 'search-container' }); + scope.appEvent('show-dash-search'); }, { inputDisabled: true }); keyboardManager.bind('ctrl+o', function() { diff --git a/src/app/partials/dashboard.html b/src/app/partials/dashboard.html index b4c0c517968..b599f1e211d 100644 --- a/src/app/partials/dashboard.html +++ b/src/app/partials/dashboard.html @@ -8,8 +8,8 @@
    -
    -
    +
    +
    diff --git a/src/app/partials/search.html b/src/app/partials/search.html index a595c2309f0..61c06229b04 100644 --- a/src/app/partials/search.html +++ b/src/app/partials/search.html @@ -7,11 +7,11 @@
    - + starred | - + tags diff --git a/src/css/less/bootswatch.dark.less b/src/css/less/bootswatch.dark.less index fcdb05fef4d..28c48cb1cb5 100644 --- a/src/css/less/bootswatch.dark.less +++ b/src/css/less/bootswatch.dark.less @@ -26,11 +26,11 @@ blockquote { // ----------------------------------------------------- html { - min-height: 100%; + height: 100%; } body { - min-height: 100%; + height: 100%; //#gradient > .vertical (@bodyBackground, #252A30); //background: @bodyBackground; background: @bodyBackground; diff --git a/src/css/less/search.less b/src/css/less/search.less index a944b57caee..4798aadabda 100644 --- a/src/css/less/search.less +++ b/src/css/less/search.less @@ -1,4 +1,4 @@ -.gf-box.search-container { +.search-container { left: 52px; top: 33px; margin: 15px; @@ -7,6 +7,8 @@ width: 700px; box-shadow: 0px 0px 55px 0px black; padding: 10px; + background-color: @grafanaPanelBackground; + border: 1px solid @grafanaTargetFuncBackground; .label-tag { margin-left: 6px; From 26eb6e559ee257c2fa106573b49af4179e77a77c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 17 Feb 2015 18:43:37 +0100 Subject: [PATCH 062/118] Removed use of absolute urls in dashboard search and dashlist, root_url option is now not required, it is required if you use oauth or proxy grafana under suburl, #1483 --- pkg/api/search.go | 4 ---- pkg/models/search.go | 1 - src/app/controllers/search.js | 22 ++++++++++------------ src/app/panels/dashlist/module.html | 2 +- 4 files changed, 11 insertions(+), 18 deletions(-) diff --git a/pkg/api/search.go b/pkg/api/search.go index d0f93851d79..f180482fd0e 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -4,7 +4,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" ) // TODO: this needs to be cached or improved somehow @@ -76,9 +75,6 @@ func Search(c *middleware.Context) { } result.Dashboards = query.Result - for _, dash := range result.Dashboards { - dash.Url = setting.ToAbsUrl("dashboard/db/" + dash.Slug) - } } c.JSON(200, result) diff --git a/pkg/models/search.go b/pkg/models/search.go index 4f5b24ff084..0265d48b32d 100644 --- a/pkg/models/search.go +++ b/pkg/models/search.go @@ -11,7 +11,6 @@ type DashboardSearchHit struct { Title string `json:"title"` Slug string `json:"slug"` Tags []string `json:"tags"` - Url string `json:"url"` IsStarred bool `json:"isStarred"` } diff --git a/src/app/controllers/search.js b/src/app/controllers/search.js index 3819be3a2e7..e724aadcf7f 100644 --- a/src/app/controllers/search.js +++ b/src/app/controllers/search.js @@ -48,7 +48,7 @@ function (angular, _, config) { var selectedDash = $scope.results.dashboards[$scope.selectedIndex]; if (selectedDash) { $location.search({}); - $location.path("/dashboard/db/" + selectedDash.slug); + $location.path(selectedDash.url); } } }; @@ -57,11 +57,6 @@ function (angular, _, config) { $scope.selectedIndex = Math.max(Math.min($scope.selectedIndex + direction, $scope.resultCount - 1), 0); }; - $scope.goToDashboard = function(slug) { - $location.search({}); - $location.path("/dashboard/db/" + slug); - }; - $scope.searchDashboards = function() { $scope.currentSearchId = $scope.currentSearchId + 1; var localSearchId = $scope.currentSearchId; @@ -70,13 +65,16 @@ function (angular, _, config) { .then(function(results) { if (localSearchId < $scope.currentSearchId) { return; } - if ($scope.query.query === "" && !$scope.query.starred) { - results.dashboards.unshift({ title: 'Home', url: config.appSubUrl + '/', isHome: true }); - } - - $scope.results.dashboards = results.dashboards; - $scope.results.tags = results.tags; $scope.resultCount = results.tagsOnly ? results.tags.length : results.dashboards.length; + $scope.results.tags = results.tags; + $scope.results.dashboards = _.map(results.dashboards, function(dash) { + dash.url = 'dashboard/db/' + dash.slug; + return dash; + }); + + if ($scope.query.query === "" && !$scope.query.starred) { + $scope.results.dashboards.unshift({ title: 'Home', url: config.appSubUrl + '/', isHome: true }); + } }); }; diff --git a/src/app/panels/dashlist/module.html b/src/app/panels/dashlist/module.html index c02e993984a..b59822e09a2 100644 --- a/src/app/panels/dashlist/module.html +++ b/src/app/panels/dashlist/module.html @@ -1,7 +1,7 @@
    - + {{dash.title}} From ad13fd05426068f1b263e00414fb2f5569b3a62a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Feb 2015 09:08:12 +0100 Subject: [PATCH 063/118] Added validation for correct timespans when using panel timeshift or relative time override, Fixes #1494 --- src/app/components/kbn.js | 5 +++++ src/app/panels/graph/module.js | 24 +++++++++++++++++++----- src/test/specs/kbn-format-specs.js | 7 +++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/app/components/kbn.js b/src/app/components/kbn.js index fa375f83f6e..562ef451a2a 100644 --- a/src/app/components/kbn.js +++ b/src/app/components/kbn.js @@ -197,6 +197,11 @@ function($, _, moment) { return kbn.parseDateMath(mathString, time); }; + kbn._timespanRegex = /^\d+[h,m,M,w,s,H,d]$/; + kbn.isValidTimeSpan = function(str) { + return kbn._timespanRegex.test(str); + }; + kbn.parseDateMath = function(mathString, time, roundUp) { var dateTime = moment(time); for (var i = 0; i < mathString.length;) { diff --git a/src/app/panels/graph/module.js b/src/app/panels/graph/module.js index 0361633a9f7..1b8344ed92d 100644 --- a/src/app/panels/graph/module.js +++ b/src/app/panels/graph/module.js @@ -123,14 +123,16 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { $scope.render(); }; - $scope.updateTimeRange = function () { - $scope.range = timeSrv.timeRange(); - $scope.rangeUnparsed = timeSrv.timeRange(false); - - $scope.panelMeta.timeInfo = ""; + $scope.applyPanelTimeOverrides = function() { + $scope.panelMeta.timeInfo = ''; // check panel time overrrides if ($scope.panel.timeFrom) { + if (!kbn.isValidTimeSpan($scope.panel.timeFrom)) { + $scope.panelMeta.timeInfo = 'invalid time override'; + return; + } + if (_.isString($scope.rangeUnparsed.from)) { $scope.panelMeta.timeInfo = "last " + $scope.panel.timeFrom; $scope.rangeUnparsed.from = 'now-' + $scope.panel.timeFrom; @@ -139,12 +141,24 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { } if ($scope.panel.timeShift) { + if (!kbn.isValidTimeSpan($scope.panel.timeFrom)) { + $scope.panelMeta.timeInfo = 'invalid timeshift'; + return; + } + var timeShift = '-' + $scope.panel.timeShift; $scope.panelMeta.timeInfo += ' timeshift ' + timeShift; $scope.range.from = kbn.parseDateMath(timeShift, $scope.range.from); $scope.range.to = kbn.parseDateMath(timeShift, $scope.range.to); + $scope.rangeUnparsed = $scope.range; } + }; + + $scope.updateTimeRange = function () { + $scope.range = timeSrv.timeRange(); + $scope.rangeUnparsed = timeSrv.timeRange(false); + $scope.applyPanelTimeOverrides(); if ($scope.panel.maxDataPoints) { $scope.resolution = $scope.panel.maxDataPoints; diff --git a/src/test/specs/kbn-format-specs.js b/src/test/specs/kbn-format-specs.js index 7c75e7eca93..726ef567327 100644 --- a/src/test/specs/kbn-format-specs.js +++ b/src/test/specs/kbn-format-specs.js @@ -74,10 +74,17 @@ define([ var date = kbn.parseDateMath('-2d', new Date(2014,1,5)); expect(date.getTime()).to.equal(new Date(2014, 1, 3).getTime()); }); + it('should handle multiple math expressions', function() { var date = kbn.parseDateMath('-2d-6h', new Date(2014, 1, 5)); expect(date.toString()).to.equal(new Date(2014, 1, 2, 18).toString()); }); + + it('should return false when invalid expression', function() { + var date = kbn.parseDateMath('2', new Date(2014, 1, 5)); + expect(date).to.equal(false); + }); + }); }); From 5c9ef9d9dab10ed04e45a597c1b0b4ba3b7b56eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Feb 2015 09:20:39 +0100 Subject: [PATCH 064/118] Added form validation and css class for invalid timespans, #1494 --- src/app/directives/ngModelOnBlur.js | 20 +++++++++++++++++-- .../features/panel/partials/panelTime.html | 8 ++++---- src/app/panels/timepicker/editor.html | 5 ++++- src/css/less/grafana.less | 1 + src/css/less/validation.less | 3 +++ 5 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 src/css/less/validation.less diff --git a/src/app/directives/ngModelOnBlur.js b/src/app/directives/ngModelOnBlur.js index 1aea09f9dc8..8057c071bdb 100644 --- a/src/app/directives/ngModelOnBlur.js +++ b/src/app/directives/ngModelOnBlur.js @@ -1,5 +1,8 @@ -define(['angular'], -function (angular) { +define([ + 'angular', + 'kbn' +], +function (angular, kbn) { 'use strict'; angular @@ -34,5 +37,18 @@ function (angular) { }); } }; + }) + .directive('validTimeSpan', function() { + return { + require: 'ngModel', + link: function(scope, elm, attrs, ctrl) { + ctrl.$validators.integer = function(modelValue, viewValue) { + if (ctrl.$isEmpty(modelValue)) { + return true; + } + return kbn.isValidTimeSpan(viewValue); + }; + } + }; }); }); diff --git a/src/app/features/panel/partials/panelTime.html b/src/app/features/panel/partials/panelTime.html index 7b8b507a2ae..0c407cf2ff1 100644 --- a/src/app/features/panel/partials/panelTime.html +++ b/src/app/features/panel/partials/panelTime.html @@ -12,8 +12,8 @@ Last
  • -
  • @@ -31,8 +31,8 @@ Amount
  • -
  • diff --git a/src/app/panels/timepicker/editor.html b/src/app/panels/timepicker/editor.html index 33a5fbfaf00..34712ad582d 100644 --- a/src/app/panels/timepicker/editor.html +++ b/src/app/panels/timepicker/editor.html @@ -18,7 +18,10 @@
  • + ng-model="panel.nowDelay" placeholder="0m" + valid-time-span + bs-tooltip="'Enter 1m to ignore the last minute (because it can contain incomplete metrics)'" + data-placement="right">
  • diff --git a/src/css/less/grafana.less b/src/css/less/grafana.less index 7b32d09e854..55e8def8f33 100644 --- a/src/css/less/grafana.less +++ b/src/css/less/grafana.less @@ -13,6 +13,7 @@ @import "gfbox.less"; @import "dashlist.less"; @import "admin.less"; +@import "validation.less"; .row-control-inner { padding:0px; diff --git a/src/css/less/validation.less b/src/css/less/validation.less new file mode 100644 index 00000000000..a0568bfc96a --- /dev/null +++ b/src/css/less/validation.less @@ -0,0 +1,3 @@ +input[type=text].ng-invalid { + box-shadow: inset 0 0px 7px @red; +} From 596ce18aebaa848951506f029e45a0355a79ba05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Feb 2015 10:44:36 +0100 Subject: [PATCH 065/118] Worked on clone dashboard feature, #1488 --- CHANGELOG.md | 4 +++ src/app/components/settings.js | 1 - src/app/features/dashboard/all.js | 1 + .../features/dashboard/cloneDashboardCtrl.js | 33 +++++++++++++++++++ .../features/dashboard/dashboardNavCtrl.js | 30 +++++++---------- .../dashboard/partials/cloneDashboard.html | 26 +++++++++++++++ .../features/grafanaDatasource/datasource.js | 5 --- src/app/partials/dashboard_topnav.html | 1 + src/config.sample.js | 6 ---- src/css/less/forms.less | 3 +- src/css/less/tightform.less | 2 +- src/test/specs/sharePanelCtrl-specs.js | 1 - 12 files changed, 79 insertions(+), 34 deletions(-) create mode 100644 src/app/features/dashboard/cloneDashboardCtrl.js create mode 100644 src/app/features/dashboard/partials/cloneDashboard.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d5721604c4..1e1f1f9aade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ - [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 - [Issue #171](https://github.com/grafana/grafana/issues/171). Panel: Different time periods, panels can override dashboard relative time and/or add a time shift +- [Issue #1488](https://github.com/grafana/grafana/issues/1488). Dashboard: Clone dashboard / Save as + +**Changes** +- Dashboard title change & save will no longer create a new dashboard, it will just change the title. **Enhancements** - [Issue #1297](https://github.com/grafana/grafana/issues/1297). Graphite: Added cumulative and minimumBelow graphite functions diff --git a/src/app/components/settings.js b/src/app/components/settings.js index 7e896b475ea..078b5519420 100644 --- a/src/app/components/settings.js +++ b/src/app/components/settings.js @@ -27,7 +27,6 @@ function (_, crypto) { playlist_timespan: "1m", unsaved_changes_warning: true, search: { max_results: 100 }, - admin: {}, appSubUrl: "" }; diff --git a/src/app/features/dashboard/all.js b/src/app/features/dashboard/all.js index 1ba0051adf3..a64800867c2 100644 --- a/src/app/features/dashboard/all.js +++ b/src/app/features/dashboard/all.js @@ -1,6 +1,7 @@ define([ './dashboardCtrl', './dashboardNavCtrl', + './cloneDashboardCtrl', './playlistCtrl', './rowCtrl', './sharePanelCtrl', diff --git a/src/app/features/dashboard/cloneDashboardCtrl.js b/src/app/features/dashboard/cloneDashboardCtrl.js new file mode 100644 index 00000000000..855129ecaf9 --- /dev/null +++ b/src/app/features/dashboard/cloneDashboardCtrl.js @@ -0,0 +1,33 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('CloneDashboardCtrl', function($scope, datasourceSrv, $location) { + + $scope.init = function() { + $scope.db = datasourceSrv.getGrafanaDB(); + $scope.clone.id = null; + $scope.clone.editable = true; + $scope.clone.title = $scope.clone.title + " Copy"; + }; + + $scope.saveClone = function() { + $scope.db.saveDashboard($scope.clone) + .then(function(result) { + + $scope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + result.title]); + $location.url(result.url); + $scope.appEvent('dashboard-saved', $scope.clone); + $scope.dismiss(); + + }, function(err) { + $scope.appEvent('alert-error', ['Save failed', err]); + }); + }; + }); + +}); diff --git a/src/app/features/dashboard/dashboardNavCtrl.js b/src/app/features/dashboard/dashboardNavCtrl.js index b256b24d6cf..f41dd007089 100644 --- a/src/app/features/dashboard/dashboardNavCtrl.js +++ b/src/app/features/dashboard/dashboardNavCtrl.js @@ -65,27 +65,11 @@ function (angular, _, moment, config, store) { window.sessionStorage["grafanaAdminPassword"] = pwd; }; - $scope.isAdmin = function() { - if (!config.admin || !config.admin.password) { return true; } - if ($scope.passwordCache() === config.admin.password) { return true; } - - var password = window.prompt("Admin password", ""); - $scope.passwordCache(password); - - if (password === config.admin.password) { return true; } - - alertSrv.set('Save failed', 'Password incorrect', 'error'); - - return false; - }; - $scope.openSearch = function() { $scope.appEvent('show-dash-search'); }; $scope.saveDashboard = function() { - if (!$scope.isAdmin()) { return false; } - var clone = angular.copy($scope.dashboard); $scope.db.saveDashboard(clone) .then(function(result) { @@ -96,7 +80,7 @@ function (angular, _, moment, config, store) { $location.path(result.url); } - $rootScope.$emit('dashboard-saved', $scope.dashboard); + $scope.appEvent('dashboard-saved', $scope.dashboard); }, function(err) { $scope.appEvent('alert-error', ['Save failed', err]); @@ -104,8 +88,6 @@ function (angular, _, moment, config, store) { }; $scope.deleteDashboard = function() { - if (!$scope.isAdmin()) { return false; } - $scope.appEvent('confirm-modal', { title: 'Delete dashboard', text: 'Do you want to delete dashboard ' + $scope.dashboard.title + '?', @@ -123,6 +105,16 @@ function (angular, _, moment, config, store) { }); }; + $scope.cloneDashboard = function() { + var newScope = $rootScope.$new(); + newScope.clone = angular.copy($scope.dashboard); + + $scope.appEvent('show-modal', { + src: './app/features/dashboard/partials/cloneDashboard.html', + scope: newScope, + }); + }; + $scope.exportDashboard = function() { var blob = new Blob([angular.toJson($scope.dashboard, true)], { type: "application/json;charset=utf-8" }); window.saveAs(blob, $scope.dashboard.title + '-' + new Date().getTime()); diff --git a/src/app/features/dashboard/partials/cloneDashboard.html b/src/app/features/dashboard/partials/cloneDashboard.html new file mode 100644 index 00000000000..dcbea9d1d00 --- /dev/null +++ b/src/app/features/dashboard/partials/cloneDashboard.html @@ -0,0 +1,26 @@ + + diff --git a/src/app/features/grafanaDatasource/datasource.js b/src/app/features/grafanaDatasource/datasource.js index 7afee86096f..b6babaa7a93 100644 --- a/src/app/features/grafanaDatasource/datasource.js +++ b/src/app/features/grafanaDatasource/datasource.js @@ -45,11 +45,6 @@ function (angular, _, kbn) { }; GrafanaDatasource.prototype.saveDashboard = function(dashboard) { - // remove id if title has changed - if (dashboard.title !== dashboard.originalTitle) { - dashboard.id = null; - } - return backendSrv.post('/api/dashboards/db/', { dashboard: dashboard }) .then(function(data) { return { title: dashboard.title, url: '/dashboard/db/' + data.slug }; diff --git a/src/app/partials/dashboard_topnav.html b/src/app/partials/dashboard_topnav.html index 71956a8c461..39264b9fbbe 100644 --- a/src/app/partials/dashboard_topnav.html +++ b/src/app/partials/dashboard_topnav.html @@ -44,6 +44,7 @@
  • Export
  • View JSON
  • Delete dashboard
  • +
  • Clone dashboard
  • diff --git a/src/config.sample.js b/src/config.sample.js index 0c11e4007b2..ad14b7f7520 100644 --- a/src/config.sample.js +++ b/src/config.sample.js @@ -86,12 +86,6 @@ define(['settings'], function(Settings) { // Example: "1m", "1h" playlist_timespan: "1m", - // If you want to specify password before saving, please specify it below - // The purpose of this password is not security, but to stop some users from accidentally changing dashboards - admin: { - password: '' - }, - // Change window title prefix from 'Grafana - ' window_title_prefix: 'Grafana - ', diff --git a/src/css/less/forms.less b/src/css/less/forms.less index aa9381e74ff..b5e866e8045 100644 --- a/src/css/less/forms.less +++ b/src/css/less/forms.less @@ -1,7 +1,8 @@ input[type=text].input-fluid { width: 100%; box-sizing: border-box; - padding: 14px; + padding: 10px; + font-size: 16px; -moz-box-sizing: border-box; height: 100%; } diff --git a/src/css/less/tightform.less b/src/css/less/tightform.less index 22efcda2fcf..5266a81b901 100644 --- a/src/css/less/tightform.less +++ b/src/css/less/tightform.less @@ -9,7 +9,7 @@ padding: 0; margin: 0; } - &:last-child { + &:last-child, &.last { border-bottom: 1px solid @grafanaTargetBorder; } } diff --git a/src/test/specs/sharePanelCtrl-specs.js b/src/test/specs/sharePanelCtrl-specs.js index 6cb0a732d6e..acfc849db25 100644 --- a/src/test/specs/sharePanelCtrl-specs.js +++ b/src/test/specs/sharePanelCtrl-specs.js @@ -20,7 +20,6 @@ define([ describe('shareUrl with current time range and panel', function() { - it('should generate share url relative time', function() { ctx.$location.path('/test'); ctx.scope.panel = { id: 22 }; From 17004ce3ae2ee1039e429a98515b0dbe5b5abf3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Feb 2015 13:00:39 +0100 Subject: [PATCH 066/118] Fixed issue with login by username and uppercase letters in username, #1484 --- pkg/services/sqlstore/user.go | 2 +- src/css/less/validation.less | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 2034b0ff347..bfa5ebc639a 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -139,7 +139,7 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error { if strings.Contains(query.LoginOrEmail, "@") { user = &m.User{Email: query.LoginOrEmail} } else { - user = &m.User{Login: strings.ToLower(query.LoginOrEmail)} + user = &m.User{Login: query.LoginOrEmail} } has, err := x.Get(user) diff --git a/src/css/less/validation.less b/src/css/less/validation.less index a0568bfc96a..fdb398f2245 100644 --- a/src/css/less/validation.less +++ b/src/css/less/validation.less @@ -1,3 +1,3 @@ -input[type=text].ng-invalid { +input[type=text].ng-dirty.ng-invalid { box-shadow: inset 0 0px 7px @red; } From 4ed54f6aa9f938a29ddd3a9f13ce07641b22f3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Feb 2015 13:18:29 +0100 Subject: [PATCH 067/118] Force full reload when switching account, temp fix for refreshing frontend datasource, settings, and user account role --- src/app/features/profile/profileCtrl.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/features/profile/profileCtrl.js b/src/app/features/profile/profileCtrl.js index b028e48be1d..fab5367c726 100644 --- a/src/app/features/profile/profileCtrl.js +++ b/src/app/features/profile/profileCtrl.js @@ -1,7 +1,8 @@ define([ 'angular', + 'config', ], -function (angular) { +function (angular, config) { 'use strict'; var module = angular.module('grafana.controllers'); @@ -28,7 +29,9 @@ function (angular) { }; $scope.setUsingAccount = function(account) { - backendSrv.post('/api/user/using/' + account.accountId).then($scope.getUserAccounts); + backendSrv.post('/api/user/using/' + account.accountId).then(function() { + window.location.href = config.appSubUrl + '/profile'; + }); }; $scope.update = function() { From 60ae4afe8776f20e0bdfcf56ad6a6cbb7a6f5c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Feb 2015 14:06:44 +0100 Subject: [PATCH 068/118] Refresh frontend datasourceSrv after datasource update, no longer need to reload the page to use a newly added or updated datasource, #1493 --- pkg/api/api.go | 1 + pkg/api/frontendsettings.go | 12 ++- pkg/api/index.go | 2 +- src/app/components/settings.js | 71 +++++++----------- .../features/account/datasourceEditCtrl.js | 17 +++-- src/app/features/account/datasourcesCtrl.js | 6 +- src/app/features/admin/adminEditUserCtrl.js | 10 ++- src/app/services/datasourceSrv.js | 74 ++++++++----------- 8 files changed, 94 insertions(+), 99 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index f79c0661d55..b61517c9d56 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -78,6 +78,7 @@ func Register(r *macaron.Macaron) { r.Get("/:id", GetDataSourceById) }, reqAccountAdmin) + r.Get("/frontend/settings/", GetFrontendSettings) r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest) // Dashboard diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 071852a5e8e..bc909ca8ba1 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func getFrontendSettings(c *middleware.Context) (map[string]interface{}, error) { +func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, error) { accountDataSources := make([]*m.DataSource, 0) if c.IsSignedIn { @@ -71,3 +71,13 @@ func getFrontendSettings(c *middleware.Context) (map[string]interface{}, error) return jsonObj, nil } + +func GetFrontendSettings(c *middleware.Context) { + settings, err := getFrontendSettingsMap(c) + if err != nil { + c.JsonApiErr(400, "Failed to get frontend settings", err) + return + } + + c.JSON(200, settings) +} diff --git a/pkg/api/index.go b/pkg/api/index.go index 98860b26e6a..c63e65e5a39 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -7,7 +7,7 @@ import ( ) func setIndexViewData(c *middleware.Context) error { - settings, err := getFrontendSettings(c) + settings, err := getFrontendSettingsMap(c) if err != nil { return err } diff --git a/src/app/components/settings.js b/src/app/components/settings.js index 078b5519420..158d255a635 100644 --- a/src/app/components/settings.js +++ b/src/app/components/settings.js @@ -1,8 +1,7 @@ define([ 'lodash', - 'crypto', ], -function (_, crypto) { +function (_) { "use strict"; return function Settings (options) { @@ -32,49 +31,31 @@ function (_, crypto) { var settings = _.extend({}, defaults, options); - var parseBasicAuth = function(datasource) { - var passwordEnd = datasource.url.indexOf('@'); - if (passwordEnd > 0) { - var userStart = datasource.url.indexOf('//') + 2; - var userAndPassword = datasource.url.substring(userStart, passwordEnd); - var bytes = crypto.charenc.Binary.stringToBytes(userAndPassword); - datasource.basicAuth = crypto.util.bytesToBase64(bytes); - - var urlHead = datasource.url.substring(0, userStart); - datasource.url = urlHead + datasource.url.substring(passwordEnd + 1); - } - - return datasource; - }; - - var parseMultipleHosts = function(datasource) { - datasource.urls = _.map(datasource.url.split(","), function (url) { return url.trim(); }); - return datasource; - }; - - // backward compatible with old config - if (options.graphiteUrl) { - settings.datasources.graphite = { - type: 'graphite', - url: options.graphiteUrl, - default: true - }; - } - - if (options.elasticsearch) { - settings.datasources.elasticsearch = { - type: 'elasticsearch', - url: options.elasticsearch, - index: options.grafana_index, - grafanaDB: true - }; - } - - _.each(settings.datasources, function(datasource, key) { - datasource.name = key; - if (datasource.url) { parseBasicAuth(datasource); } - if (datasource.type === 'influxdb') { parseMultipleHosts(datasource); } - }); + // var parseBasicAuth = function(datasource) { + // var passwordEnd = datasource.url.indexOf('@'); + // if (passwordEnd > 0) { + // var userStart = datasource.url.indexOf('//') + 2; + // var userAndPassword = datasource.url.substring(userStart, passwordEnd); + // var bytes = crypto.charenc.Binary.stringToBytes(userAndPassword); + // datasource.basicAuth = crypto.util.bytesToBase64(bytes); + // + // var urlHead = datasource.url.substring(0, userStart); + // datasource.url = urlHead + datasource.url.substring(passwordEnd + 1); + // } + // + // return datasource; + // }; + // + // var parseMultipleHosts = function(datasource) { + // datasource.urls = _.map(datasource.url.split(","), function (url) { return url.trim(); }); + // return datasource; + // }; + // + // _.each(settings.datasources, function(datasource, key) { + // datasource.name = key; + // if (datasource.url) { parseBasicAuth(datasource); } + // if (datasource.type === 'influxdb') { parseMultipleHosts(datasource); } + // }); if (settings.plugins.panels) { _.extend(settings.panels, settings.plugins.panels); diff --git a/src/app/features/account/datasourceEditCtrl.js b/src/app/features/account/datasourceEditCtrl.js index 389bf8bc7a8..5366cd05aa5 100644 --- a/src/app/features/account/datasourceEditCtrl.js +++ b/src/app/features/account/datasourceEditCtrl.js @@ -7,7 +7,7 @@ function (angular) { var module = angular.module('grafana.controllers'); - module.controller('DataSourceEditCtrl', function($scope, $http, backendSrv, $routeParams, $location) { + module.controller('DataSourceEditCtrl', function($scope, $http, backendSrv, $routeParams, $location, datasourceSrv) { var defaults = { name: '', @@ -41,12 +41,19 @@ function (angular) { }); }; + $scope.updateFrontendSettings = function() { + backendSrv.get('/api/frontend/settings').then(function(settings) { + datasourceSrv.init(settings.datasources); + }); + }; + $scope.update = function() { if (!$scope.editForm.$valid) { return; } backendSrv.post('/api/datasources', $scope.current).then(function() { + $scope.updateFrontendSettings(); $location.path("account/datasources"); }); }; @@ -56,10 +63,10 @@ function (angular) { return; } - backendSrv.put('/api/datasources', $scope.current) - .then(function() { - $scope.getDatasources(); - }); + backendSrv.put('/api/datasources', $scope.current).then(function() { + $scope.updateFrontendSettings(); + $location.path("account/datasources"); + }); }; $scope.init(); diff --git a/src/app/features/account/datasourcesCtrl.js b/src/app/features/account/datasourcesCtrl.js index 00a906ba64e..da77df252e1 100644 --- a/src/app/features/account/datasourcesCtrl.js +++ b/src/app/features/account/datasourcesCtrl.js @@ -7,7 +7,7 @@ function (angular) { var module = angular.module('grafana.controllers'); - module.controller('DataSourcesCtrl', function($scope, $http, backendSrv) { + module.controller('DataSourcesCtrl', function($scope, $http, backendSrv, datasourceSrv) { $scope.init = function() { $scope.datasources = []; @@ -23,6 +23,10 @@ function (angular) { $scope.remove = function(ds) { backendSrv.delete('/api/datasources/' + ds.id).then(function() { $scope.getDatasources(); + + backendSrv.get('/api/frontend/settings').then(function(settings) { + datasourceSrv.init(settings.datasources); + }); }); }; diff --git a/src/app/features/admin/adminEditUserCtrl.js b/src/app/features/admin/adminEditUserCtrl.js index d01d5d8559d..86b59c4599b 100644 --- a/src/app/features/admin/adminEditUserCtrl.js +++ b/src/app/features/admin/adminEditUserCtrl.js @@ -6,7 +6,7 @@ function (angular) { var module = angular.module('grafana.controllers'); - module.controller('AdminEditUserCtrl', function($scope, $routeParams, backendSrv) { + module.controller('AdminEditUserCtrl', function($scope, $routeParams, backendSrv, $location) { $scope.user = {}; $scope.init = function() { @@ -28,9 +28,13 @@ function (angular) { $scope.update = function() { if (!$scope.userForm.$valid) { return; } if ($scope.createMode) { - backendSrv.post('/api/admin/users', $scope.user); + backendSrv.post('/api/admin/users', $scope.user).then(function() { + $location.path('/admin/users'); + }); } else { - backendSrv.put('/api/admin/users/' + $scope.user_id, $scope.user); + backendSrv.put('/api/admin/users/' + $scope.user_id, $scope.user).then(function() { + $location.path('/admin/users'); + }); } }; diff --git a/src/app/services/datasourceSrv.js b/src/app/services/datasourceSrv.js index 1a2c4db5f35..9855bec7804 100644 --- a/src/app/services/datasourceSrv.js +++ b/src/app/services/datasourceSrv.js @@ -7,97 +7,85 @@ function (angular, _, config) { 'use strict'; var module = angular.module('grafana.services'); + var typeMap = { + 'graphite': 'GraphiteDatasource', + 'influxdb': 'InfluxDatasource', + 'elasticsearch': 'ElasticDatasource', + 'opentsdb': 'OpenTSDBDatasource', + 'grafana': 'GrafanaDatasource', + }; module.service('datasourceSrv', function($q, $http, $injector) { - var datasources = {}; - var metricSources = []; - var annotationSources = []; - var grafanaDB = {}; - this.init = function() { - _.each(config.datasources, function(value, key) { + this.init = function(dsSettingList) { + config.datasources = dsSettingList; + + this.datasources = {}; + this.metricSources = []; + this.annotationSources = []; + + _.each(dsSettingList, function(value, key) { var ds = this.datasourceFactory(value); + ds.name = key; if (value.default) { this.default = ds; ds.default = true; } - datasources[key] = ds; + this.datasources[key] = ds; }, this); if (!this.default) { - this.default = datasources[_.keys(datasources)[0]]; + this.default = this.datasources[_.keys(this.datasources)[0]]; this.default.default = true; } // create list of different source types - _.each(datasources, function(value, key) { + _.each(this.datasources, function(value, key) { if (value.supportMetrics) { - metricSources.push({ + this.metricSources.push({ name: value.name, value: value.default ? null : key, default: value.default, }); } if (value.supportAnnotations) { - annotationSources.push({ - name: key, - editorSrc: value.annotationEditorSrc, - }); + this.annotationSources.push({ name: key, editorSrc: value.annotationEditorSrc }); } if (value.grafanaDB) { - grafanaDB = value; + this.grafanaDB = value; } - }); - + }, this); }; this.datasourceFactory = function(ds) { - var Datasource = null; - switch(ds.type) { - case 'graphite': - Datasource = $injector.get('GraphiteDatasource'); - break; - case 'influxdb': - Datasource = $injector.get('InfluxDatasource'); - break; - case 'opentsdb': - Datasource = $injector.get('OpenTSDBDatasource'); - break; - case 'elasticsearch': - Datasource = $injector.get('ElasticDatasource'); - break; - case 'grafana': - Datasource = $injector.get('GrafanaDatasource'); - break; - default: - Datasource = $injector.get(ds.type); - } + var type = typeMap[ds.type] || ds.type; + var Datasource = $injector.get(type); return new Datasource(ds); }; this.get = function(name) { if (!name) { return this.default; } - if (datasources[name]) { return datasources[name]; } + if (this.datasources[name]) { return this.datasources[name]; } return this.default; }; this.getAll = function() { - return datasources; + return this.datasources; }; this.getAnnotationSources = function() { - return annotationSources; + return this.annotationSources; }; this.getMetricSources = function() { - return metricSources; + return this.metricSources; }; this.getGrafanaDB = function() { - return grafanaDB; + return this.grafanaDB; }; - this.init(); + this.init(config.datasources); }); }); From 3991d9dc065b4097b16bf911fc8865b1e3d56452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Feb 2015 14:18:54 +0100 Subject: [PATCH 069/118] Added error handling to dashboard imports, #1493 --- src/app/features/account/importCtrl.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/features/account/importCtrl.js b/src/app/features/account/importCtrl.js index a8548491c60..f14374d2db1 100644 --- a/src/app/features/account/importCtrl.js +++ b/src/app/features/account/importCtrl.js @@ -43,6 +43,10 @@ function (angular, _) { $scope.importing = true; $scope.imported = []; $scope.next(); + }, function(err) { + var resp = err.message || err.statusText || 'Unknown error'; + var message = "Failed to load dashboards from selected data source, response from server was: " + resp; + $scope.appEvent('alert-error', ['Import failed', message]); }); }; From 2ca849c0c48f7086402e9bc50bb0501aa20e4fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Feb 2015 14:43:58 +0100 Subject: [PATCH 070/118] Changed docker image to debian:jessie --- docker/production/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile index 19db258d32a..5017c2c1a36 100644 --- a/docker/production/Dockerfile +++ b/docker/production/Dockerfile @@ -1,4 +1,4 @@ -FROM phusion/baseimage +FROM debian:jessie RUN apt-get -y update RUN apt-get -y install libfontconfig From 978b12b0dcb5ad76fe2fff8e4e7341eb153e7120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Feb 2015 15:17:31 +0100 Subject: [PATCH 071/118] Close dashboard edit views when clicking on dashboard title, added some tooltips to dashboard top nav --- src/app/features/dashboard/dashboardNavCtrl.js | 4 ++++ src/app/partials/dashboard_topnav.html | 6 +++--- src/app/partials/dasheditor.html | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/app/features/dashboard/dashboardNavCtrl.js b/src/app/features/dashboard/dashboardNavCtrl.js index f41dd007089..bbe22fc0b67 100644 --- a/src/app/features/dashboard/dashboardNavCtrl.js +++ b/src/app/features/dashboard/dashboardNavCtrl.js @@ -69,6 +69,10 @@ function (angular, _, moment, config, store) { $scope.appEvent('show-dash-search'); }; + $scope.dashboardTitleAction = function() { + $scope.appEvent('hide-dash-editor'); + }; + $scope.saveDashboard = function() { var clone = angular.copy($scope.dashboard); $scope.db.saveDashboard(clone) diff --git a/src/app/partials/dashboard_topnav.html b/src/app/partials/dashboard_topnav.html index 39264b9fbbe..9f8692447c1 100644 --- a/src/app/partials/dashboard_topnav.html +++ b/src/app/partials/dashboard_topnav.html @@ -18,7 +18,7 @@ - + {{dashboard.title}}
    @@ -30,10 +30,10 @@
  • - +
  • - +
  • +
    From 0d165e67c939bea9ba035bcee9cd6952b4e0e35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Feb 2015 15:22:45 +0100 Subject: [PATCH 072/118] Fixed issue with influxdb, broken in recent commit today --- src/app/features/influxdb/datasource.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/features/influxdb/datasource.js b/src/app/features/influxdb/datasource.js index c867fb2a4a5..77863b73d62 100644 --- a/src/app/features/influxdb/datasource.js +++ b/src/app/features/influxdb/datasource.js @@ -16,7 +16,9 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { function InfluxDatasource(datasource) { this.type = 'influxdb'; - this.urls = datasource.urls; + this.urls = _.map(datasource.url.split(','), function(url) { + return url.trim(); + }); this.username = datasource.username; this.password = datasource.password; this.name = datasource.name; From 181542249daaa0a022bc5d37f1eba36e74117d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Feb 2015 16:09:17 +0100 Subject: [PATCH 073/118] Fixed failing influxdb-datasource test --- src/app/components/settings.js | 5 ----- src/test/specs/influxdb-datasource-specs.js | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/app/components/settings.js b/src/app/components/settings.js index 158d255a635..5530d34f8fa 100644 --- a/src/app/components/settings.js +++ b/src/app/components/settings.js @@ -46,11 +46,6 @@ function (_) { // return datasource; // }; // - // var parseMultipleHosts = function(datasource) { - // datasource.urls = _.map(datasource.url.split(","), function (url) { return url.trim(); }); - // return datasource; - // }; - // // _.each(settings.datasources, function(datasource, key) { // datasource.name = key; // if (datasource.url) { parseBasicAuth(datasource); } diff --git a/src/test/specs/influxdb-datasource-specs.js b/src/test/specs/influxdb-datasource-specs.js index 028423ac1f4..335ca52d81c 100644 --- a/src/test/specs/influxdb-datasource-specs.js +++ b/src/test/specs/influxdb-datasource-specs.js @@ -11,7 +11,7 @@ define([ beforeEach(ctx.providePhase(['templateSrv'])); beforeEach(ctx.createService('InfluxDatasource')); beforeEach(function() { - ctx.ds = new ctx.service({ urls: [''], user: 'test', password: 'mupp' }); + ctx.ds = new ctx.service({ url: '', user: 'test', password: 'mupp' }); }); describe('When querying influxdb with one target using query editor target spec', function() { From 0786977bcfee7a4011d0e2c84871db7fc94eb6cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 19 Feb 2015 10:15:24 +0100 Subject: [PATCH 074/118] Fixed css issue with sidebar --- src/css/less/navbar.less | 2 +- src/css/less/sidemenu.less | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/css/less/navbar.less b/src/css/less/navbar.less index 1c156d56a83..0eb59d2391f 100644 --- a/src/css/less/navbar.less +++ b/src/css/less/navbar.less @@ -89,7 +89,7 @@ .top-nav-icon { margin: 5px 0px 0 11px; - line-height: 34px; + line-height: 41px; float: left; } diff --git a/src/css/less/sidemenu.less b/src/css/less/sidemenu.less index a90224c9fa9..52323266836 100644 --- a/src/css/less/sidemenu.less +++ b/src/css/less/sidemenu.less @@ -94,7 +94,7 @@ opacity: .7; position: relative; left: 11px; - top: 5px; + top: 1px; font-size: 125%; } } @@ -104,7 +104,7 @@ .sidemenu-item { color: #f80; - line-height: 34px; + line-height: 41px; padding: 14px 10px 14px 20px; display: block; @@ -113,8 +113,6 @@ &.no-icon { padding-left: 59px; } - //text-shadow: 1px 0 10px #A25803; - //text-shadow: 1px 0 10px #f80; } } From afc52f57a22e9eb7930f8f04375c1063449fcf87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 19 Feb 2015 10:42:13 +0100 Subject: [PATCH 075/118] small fix to confirm modal --- src/app/services/alertSrv.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/services/alertSrv.js b/src/app/services/alertSrv.js index 6d5801ad649..4d22c998401 100644 --- a/src/app/services/alertSrv.js +++ b/src/app/services/alertSrv.js @@ -67,7 +67,7 @@ function (angular, _) { var confirmModal = $modal({ template: './app/partials/confirm_modal.html', - persist: true, + persist: false, modalClass: 'confirm-modal', show: false, scope: scope, From 47c6c6e1b721026b377dd08b89696b8a25f05efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 19 Feb 2015 16:09:49 +0100 Subject: [PATCH 076/118] Added change password feature, Closes #1455 --- pkg/api/api.go | 1 + pkg/api/user.go | 31 ++++++++++ pkg/models/user.go | 7 +++ pkg/services/sqlstore/user.go | 17 ++++++ src/app/features/all.js | 1 + src/app/features/opentsdb/datasource.js | 2 +- .../features/profile/changePasswordCtrl.js | 28 ++++++++++ .../features/profile/partials/password.html | 56 +++++++++++++++++++ .../features/profile/partials/profile.html | 8 ++- src/app/features/profile/profileCtrl.js | 2 +- src/app/routes/backend/all.js | 4 ++ 11 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 src/app/features/profile/changePasswordCtrl.js create mode 100644 src/app/features/profile/partials/password.html diff --git a/pkg/api/api.go b/pkg/api/api.go index b61517c9d56..c6d050547ef 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -50,6 +50,7 @@ func Register(r *macaron.Macaron) { r.Get("/accounts", GetUserAccounts) r.Post("/stars/dashboard/:id", StarDashboard) r.Delete("/stars/dashboard/:id", UnstarDashboard) + r.Put("/password", bind(m.ChangeUserPasswordCommand{}), ChangeUserPassword) }) // account diff --git a/pkg/api/user.go b/pkg/api/user.go index 306f7c7b3d4..c7f8214679b 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -4,6 +4,7 @@ import ( "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 GetUser(c *middleware.Context) { @@ -84,3 +85,33 @@ func SetUsingAccount(c *middleware.Context) { c.JsonOK("Active account changed") } + +func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) { + userQuery := m.GetUserByIdQuery{Id: c.UserId} + + if err := bus.Dispatch(&userQuery); err != nil { + c.JsonApiErr(500, "Could not read user from database", err) + return + } + + passwordHashed := util.EncodePassword(cmd.OldPassword, userQuery.Result.Salt) + if passwordHashed != userQuery.Result.Password { + c.JsonApiErr(401, "Invalid old password", nil) + return + } + + if len(cmd.NewPassword) < 4 { + c.JsonApiErr(400, "New password too short", nil) + return + } + + cmd.UserId = c.UserId + cmd.NewPassword = util.EncodePassword(cmd.NewPassword, userQuery.Result.Salt) + + if err := bus.Dispatch(&cmd); err != nil { + c.JsonApiErr(500, "Failed to change user password", err) + return + } + + c.JsonOK("User password changed") +} diff --git a/pkg/models/user.go b/pkg/models/user.go index 81ed0123c1e..2905dce8ba9 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -52,6 +52,13 @@ type UpdateUserCommand struct { UserId int64 `json:"-"` } +type ChangeUserPasswordCommand struct { + OldPassword string `json:"oldPassword"` + NewPassword string `json:"newPassword"` + + UserId int64 `json:"-"` +} + type DeleteUserCommand struct { UserId int64 } diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index bfa5ebc639a..f8773eb26cf 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -18,6 +18,7 @@ func init() { bus.AddHandler("sql", CreateUser) bus.AddHandler("sql", GetUserById) bus.AddHandler("sql", UpdateUser) + bus.AddHandler("sql", ChangeUserPassword) bus.AddHandler("sql", GetUserByLogin) bus.AddHandler("sql", SetUsingAccount) bus.AddHandler("sql", GetUserInfo) @@ -181,6 +182,22 @@ func UpdateUser(cmd *m.UpdateUserCommand) error { }) } +func ChangeUserPassword(cmd *m.ChangeUserPasswordCommand) error { + return inTransaction2(func(sess *session) error { + + user := m.User{ + Password: cmd.NewPassword, + Updated: time.Now(), + } + + if _, err := sess.Id(cmd.UserId).Update(&user); err != nil { + return err + } + + return nil + }) +} + func SetUsingAccount(cmd *m.SetUsingAccountCommand) error { return inTransaction(func(sess *xorm.Session) error { user := m.User{} diff --git a/src/app/features/all.js b/src/app/features/all.js index 635b4b5b527..004d3eebe9f 100644 --- a/src/app/features/all.js +++ b/src/app/features/all.js @@ -9,6 +9,7 @@ define([ './dashboard/all', './panel/all', './profile/profileCtrl', + './profile/changePasswordCtrl', './account/all', './admin/all', './grafanaDatasource/datasource', diff --git a/src/app/features/opentsdb/datasource.js b/src/app/features/opentsdb/datasource.js index d5bdd011dcf..16cb58f94cb 100644 --- a/src/app/features/opentsdb/datasource.js +++ b/src/app/features/opentsdb/datasource.js @@ -120,7 +120,7 @@ function (angular, _, kbn) { } function convertTargetToQuery(target) { - if (!target.metric) { + if (!target.metric || target.hide) { return null; } diff --git a/src/app/features/profile/changePasswordCtrl.js b/src/app/features/profile/changePasswordCtrl.js new file mode 100644 index 00000000000..b2bb90ea6cf --- /dev/null +++ b/src/app/features/profile/changePasswordCtrl.js @@ -0,0 +1,28 @@ +define([ + 'angular', + 'config', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('ChangePasswordCtrl', function($scope, backendSrv, $location) { + + $scope.command = {}; + + $scope.changePassword = function() { + if (!$scope.userForm.$valid) { return; } + + if ($scope.command.newPassword !== $scope.command.confirmNew) { + $scope.appEvent('alert-warning', ['New passwords do not match', '']); + return; + } + + backendSrv.put('/api/user/password', $scope.command).then(function() { + $location.path("profile"); + }); + }; + + }); +}); diff --git a/src/app/features/profile/partials/password.html b/src/app/features/profile/partials/password.html new file mode 100644 index 00000000000..f7b5a18e5d0 --- /dev/null +++ b/src/app/features/profile/partials/password.html @@ -0,0 +1,56 @@ + + + + +
    +
    + +

    Change password

    + +
    +
    +
    +
      +
    • + Old Password +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + New Password +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + Confirm New +
    • +
    • + +
    • +
    +
    +
    +
    + +
    + +
    + +
    +
    + diff --git a/src/app/features/profile/partials/profile.html b/src/app/features/profile/partials/profile.html index db9d9b52702..ac9b5298774 100644 --- a/src/app/features/profile/partials/profile.html +++ b/src/app/features/profile/partials/profile.html @@ -1,6 +1,8 @@ - + +
    diff --git a/src/app/features/profile/profileCtrl.js b/src/app/features/profile/profileCtrl.js index fab5367c726..dd957fb1228 100644 --- a/src/app/features/profile/profileCtrl.js +++ b/src/app/features/profile/profileCtrl.js @@ -7,7 +7,7 @@ function (angular, config) { var module = angular.module('grafana.controllers'); - module.controller('ProfileCtrl', function($scope, $http, backendSrv) { + module.controller('ProfileCtrl', function($scope, backendSrv) { $scope.newAccount = {name: ''}; diff --git a/src/app/routes/backend/all.js b/src/app/routes/backend/all.js index b211550d841..510f4ffb16c 100644 --- a/src/app/routes/backend/all.js +++ b/src/app/routes/backend/all.js @@ -62,6 +62,10 @@ define([ templateUrl: 'app/features/profile/partials/profile.html', controller : 'ProfileCtrl', }) + .when('/profile/password', { + templateUrl: 'app/features/profile/partials/password.html', + controller : 'ChangePasswordCtrl', + }) .when('/admin/settings', { templateUrl: 'app/features/admin/partials/settings.html', controller : 'AdminSettingsCtrl', From 596dfc304dfdf074595fc11e82074d8681e35b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 20 Feb 2015 10:45:23 +0100 Subject: [PATCH 077/118] Added guard for template variables missing query field, Fixes #1501 --- src/app/features/templating/templateSrv.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/features/templating/templateSrv.js b/src/app/features/templating/templateSrv.js index c44ff0a5669..8fc45097944 100644 --- a/src/app/features/templating/templateSrv.js +++ b/src/app/features/templating/templateSrv.js @@ -45,6 +45,9 @@ function (angular, _) { }; this.containsVariable = function(str, variableName) { + if (!str) { + return false; + } return str.indexOf('$' + variableName) !== -1 || str.indexOf('[[' + variableName + ']]') !== -1; }; From 923e18b2b997f0dee4e4940b60dff2dff7c0be1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 20 Feb 2015 11:27:57 +0100 Subject: [PATCH 078/118] Small design change for the submenu (templating, annotations), #1503 --- src/app/directives/templateParamSelector.js | 2 +- src/app/partials/submenu.html | 6 ++++- src/css/less/submenu.less | 4 +++ src/css/less/tightform.less | 27 ++++++++++++++------- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/app/directives/templateParamSelector.js b/src/app/directives/templateParamSelector.js index 5ff6eaa0038..8a09e814f28 100644 --- a/src/app/directives/templateParamSelector.js +++ b/src/app/directives/templateParamSelector.js @@ -14,7 +14,7 @@ function (angular, app, _, $) { ' class="tight-form-clear-input input-medium"' + ' spellcheck="false" style="display:none">'; - var buttonTemplate = '{{variable.current.text}}'; + var buttonTemplate = '{{variable.current.text}} '; return { link: function($scope, elem) { diff --git a/src/app/partials/submenu.html b/src/app/partials/submenu.html index f6e4b90f477..b822d576523 100644 --- a/src/app/partials/submenu.html +++ b/src/app/partials/submenu.html @@ -1,7 +1,10 @@
    -
    -
    - - -
    - - - -
    - -
    -
    -
    - -
    -
    +
    diff --git a/src/app/partials/submenu.html b/src/app/partials/submenu.html index b822d576523..1012a42919c 100644 --- a/src/app/partials/submenu.html +++ b/src/app/partials/submenu.html @@ -1,7 +1,8 @@
    diff --git a/src/css/less/grafana.less b/src/css/less/grafana.less index 55e8def8f33..f981201fa20 100644 --- a/src/css/less/grafana.less +++ b/src/css/less/grafana.less @@ -26,9 +26,6 @@ .row-tab { display: none; } - .submenu-controls { - display: none; - } .add-row-panel-hint { display: none; } diff --git a/src/css/less/submenu.less b/src/css/less/submenu.less index 3e919890442..f560acf175c 100644 --- a/src/css/less/submenu.less +++ b/src/css/less/submenu.less @@ -1,11 +1,11 @@ -.submenu-controls-visible:not(.hide-controls) { +.submenu-controls-visible { .panel-fullscreen { top: 88px; } } .submenu-controls { - margin: 5px 5px 0 10px; + margin: 5px 10px 0 10px; } .annotation-disabled, .annotation-disabled a { diff --git a/src/test/specs/dashboardSrv-specs.js b/src/test/specs/dashboardSrv-specs.js index 739d66871bc..2254739ac54 100644 --- a/src/test/specs/dashboardSrv-specs.js +++ b/src/test/specs/dashboardSrv-specs.js @@ -157,8 +157,6 @@ define([ }); it('should move pulldowns to new schema', function() { - expect(model.templating.enable).to.be(true); - expect(model.annotations.enable).to.be(true); expect(model.annotations.list[0].name).to.be('old'); }); From 481a4b0f1be05d82833505a1ebcc6d37480263a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 20 Feb 2015 12:25:54 +0100 Subject: [PATCH 080/118] Small fix to unsavedChangesSrv, did not handle new template variable correctly --- src/app/features/dashboard/unsavedChangesSrv.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/features/dashboard/unsavedChangesSrv.js b/src/app/features/dashboard/unsavedChangesSrv.js index f1782a35ea3..a2e419720f4 100644 --- a/src/app/features/dashboard/unsavedChangesSrv.js +++ b/src/app/features/dashboard/unsavedChangesSrv.js @@ -88,8 +88,11 @@ function(angular, _, config) { _.each(current.templating.list, function(value, index) { value.current = null; value.options = null; - original.templating.list[index].current = null; - original.templating.list[index].options = null; + + if (original.templating.list.length > index) { + original.templating.list[index].current = null; + original.templating.list[index].options = null; + } }); var currentTimepicker = _.findWhere(current.nav, { type: 'timepicker' }); From 7a1405405764cef5005adc78ff39b118f60e8093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 20 Feb 2015 14:07:24 +0100 Subject: [PATCH 081/118] Graph: Shared tooltip improvement, can now support metrics of different resolution/intervals, Closes #978, Fixes #1499 --- CHANGELOG.md | 1 + src/app/panels/graph/graph.tooltip.js | 54 ++++----------------------- src/test/specs/graph-tooltip-specs.js | 18 --------- 3 files changed, 9 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1f1f9aade..dc509eb001a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Dashboard title change & save will no longer create a new dashboard, it will just change the title. **Enhancements** +- [Issue #978](https://github.com/grafana/grafana/issues/978). Graph: Shared tooltip improvement, can now support metrics of different resolution/intervals - [Issue #1297](https://github.com/grafana/grafana/issues/1297). Graphite: Added cumulative and minimumBelow graphite functions - [Issue #1296](https://github.com/grafana/grafana/issues/1296). InfluxDB: Auto escape column names with special characters. Thanks @steven-aerts - [Issue #1321](https://github.com/grafana/grafana/issues/1321). SingleStatPanel: You can now use template variables in pre & postfix diff --git a/src/app/panels/graph/graph.tooltip.js b/src/app/panels/graph/graph.tooltip.js index 9e0ff0b8c7b..d4690bea96d 100644 --- a/src/app/panels/graph/graph.tooltip.js +++ b/src/app/panels/graph/graph.tooltip.js @@ -9,7 +9,7 @@ function ($) { var $tooltip = $('
    '); - this.findHoverIndexFromDataPoints = function(posX, series,last) { + this.findHoverIndexFromDataPoints = function(posX, series, last) { var ps = series.datapoints.pointsize; var initial = last*ps; var len = series.datapoints.points.length; @@ -38,34 +38,10 @@ function ($) { }; this.getMultiSeriesPlotHoverInfo = function(seriesList, pos) { - var value, i, series, hoverIndex, seriesTmp; + var value, i, series, hoverIndex; var results = []; - var pointCount; - for (i = 0; i < seriesList.length; i++) { - seriesTmp = seriesList[i]; - if (!seriesTmp.data.length) { continue; } - - if (!pointCount) { - series = seriesTmp; - pointCount = series.data.length; - continue; - } - - if (seriesTmp.data.length !== pointCount) { - results.pointCountMismatch = true; - return results; - } - } - - hoverIndex = this.findHoverIndexFromData(pos.x, series); - var lasthoverIndex = 0; - if(!scope.panel.steppedLine) { - lasthoverIndex = hoverIndex; - } - //now we know the current X (j) position for X and Y values - results.time = series.data[hoverIndex][0]; var last_value = 0; //needed for stacked values for (i = 0; i < seriesList.length; i++) { @@ -76,6 +52,9 @@ function ($) { continue; } + hoverIndex = this.findHoverIndexFromData(pos.x, series); + results.time = series.data[hoverIndex][0]; + if (scope.panel.stack) { if (scope.panel.tooltip.value_type === 'individual') { value = series.data[hoverIndex][1]; @@ -90,19 +69,9 @@ function ($) { // Highlighting multiple Points depending on the plot type if (scope.panel.steppedLine || (scope.panel.stack && scope.panel.nullPointMode == "null")) { // stacked and steppedLine plots can have series with different length. - // Stacked series can increase its length on each new stacked serie if null points found, - // to speed the index search we begin always on the las found hoverIndex. - var newhoverIndex = this.findHoverIndexFromDataPoints(pos.x, series,lasthoverIndex); - // update lasthoverIndex depends also on the plot type. - if(!scope.panel.steppedLine) { - // on stacked graphs new will be always greater than last - lasthoverIndex = newhoverIndex; - } else { - // if steppeLine, not always series increases its length, so we should begin - // to search correct index from the original hoverIndex on each serie. - lasthoverIndex = hoverIndex; - } - + // Stacked series can increase its length on each new stacked serie if null points found, + // to speed the index search we begin always on the last found hoverIndex. + var newhoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex); results.push({ value: value, hoverIndex: newhoverIndex}); } else { results.push({ value: value, hoverIndex: hoverIndex}); @@ -141,13 +110,6 @@ function ($) { plot.unhighlight(); var seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos); - if (seriesHoverInfo.pointCountMismatch) { - self.showTooltip('Shared tooltip error', '
      ' + - '
    • Series point counts are not the same
    • ' + - '
    • Set null point mode to null or null as zero
    • ' + - '
    • For influxdb users set fill(0) in your query
    ', pos); - return; - } seriesHtml = ''; timestamp = dashboard.formatDate(seriesHoverInfo.time); diff --git a/src/test/specs/graph-tooltip-specs.js b/src/test/specs/graph-tooltip-specs.js index 155469f18a7..da02b9f2e0a 100644 --- a/src/test/specs/graph-tooltip-specs.js +++ b/src/test/specs/graph-tooltip-specs.js @@ -60,20 +60,6 @@ define([ }); }); - describeSharedTooltip("point count missmatch", function(ctx) { - ctx.setup(function() { - ctx.data = [ - { data: [[10, 15], [12, 20]], }, - { data: [[10, 2]] } - ]; - ctx.pos = { x: 11 }; - }); - - it('should set pointCountMismatch to true', function() { - expect(ctx.results.pointCountMismatch).to.be(true); - }); - }); - describeSharedTooltip("one series is hidden", function(ctx) { ctx.setup(function() { ctx.data = [ @@ -82,10 +68,6 @@ define([ ]; ctx.pos = { x: 11 }; }); - - it('should set pointCountMismatch to false', function() { - expect(ctx.results.pointCountMismatch).to.be(undefined); - }); }); describeSharedTooltip("steppedLine false, stack true, individual false", function(ctx) { From 5b1727bcba39dc70da471acdbbaaf93755698a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 20 Feb 2015 14:45:00 +0100 Subject: [PATCH 082/118] =?UTF-8?q?Graph=20&=20Singlestat:=20Support=20for?= =?UTF-8?q?=20additional=20units,=20Fahrenheit=20(=C2=B0F)=20and=20Celsius?= =?UTF-8?q?=20(=C2=B0C),=20Humidity=20(%H),=20kW,=20watt-hour=20(Wh),=20ki?= =?UTF-8?q?lowatt-hour=20(kWh),=20velocities=20(m/s,=20km/h,=20mpg,=20knot?= =?UTF-8?q?),=20Closes=20#1366?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + src/app/components/kbn.js | 46 +++++++++++++++++++++++----- src/app/panels/graph/axisEditor.html | 4 +-- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc509eb001a..7752dd3b210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Dashboard title change & save will no longer create a new dashboard, it will just change the title. **Enhancements** +- [Issue #1366](https://github.com/grafana/grafana/issues/1366). Graph & Singlestat: Support for additional units, Fahrenheit (°F) and Celsius (°C), Humidity (%H), kW, watt-hour (Wh), kilowatt-hour (kWh), velocities (m/s, km/h, mpg, knot) - [Issue #978](https://github.com/grafana/grafana/issues/978). Graph: Shared tooltip improvement, can now support metrics of different resolution/intervals - [Issue #1297](https://github.com/grafana/grafana/issues/1297). Graphite: Added cumulative and minimumBelow graphite functions - [Issue #1296](https://github.com/grafana/grafana/issues/1296). InfluxDB: Auto escape column names with special characters. Thanks @steven-aerts diff --git a/src/app/components/kbn.js b/src/app/components/kbn.js index 562ef451a2a..6d3c1e2096e 100644 --- a/src/app/components/kbn.js +++ b/src/app/components/kbn.js @@ -372,10 +372,21 @@ function($, _, moment) { kbn.valueFormats.bps = kbn.formatFuncCreator(1000, [' bps', ' Kbps', ' Mbps', ' Gbps', ' Tbps', ' Pbps', ' Ebps', ' Zbps', ' Ybps']); kbn.valueFormats.Bps = kbn.formatFuncCreator(1000, [' Bps', ' KBps', ' MBps', ' GBps', ' TBps', ' PBps', ' EBps', ' ZBps', ' YBps']); kbn.valueFormats.short = kbn.formatFuncCreator(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Qaudr', ' Quint', ' Sext', ' Sept']); - kbn.valueFormats.joule = kbn.formatFuncCreator(1000, [' J', ' kJ', ' MJ', 'GJ', 'TJ', 'PJ', 'EJ', 'ZJ', 'YJ']); - kbn.valueFormats.watt = kbn.formatFuncCreator(1000, [' W', ' kW', ' MW', 'GW', 'TW', 'PW', 'EW', 'ZW', 'YW']); + kbn.valueFormats.joule = kbn.formatFuncCreator(1000, [' J', ' kJ', ' MJ', ' GJ', ' TJ', ' PJ', ' EJ', ' ZJ', ' YJ']); + kbn.valueFormats.watt = kbn.formatFuncCreator(1000, [' W', ' kW', ' MW', ' GW', ' TW', ' PW', ' EW', ' ZW', ' YW']); + kbn.valueFormats.kwatt = kbn.formatFuncCreator(1000, [' kW', ' MW', ' GW', ' TW', ' PW', ' EW', ' ZW', ' YW']); + kbn.valueFormats.watth = kbn.formatFuncCreator(1000, [' Wh', ' kWh', ' MWh', ' GWh', ' TWh', ' PWh', ' EWh', ' ZWh', ' YWh']); + kbn.valueFormats.kwatth = kbn.formatFuncCreator(1000, [' kWh', ' MWh', ' GWh', ' TWh', ' PWh', ' EWh', ' ZWh', ' YWh']); kbn.valueFormats.ev = kbn.formatFuncCreator(1000, [' eV', ' keV', ' MeV', 'GeV', 'TeV', 'PeV', 'EeV', 'ZeV', 'YeV']); kbn.valueFormats.none = kbn.toFixed; + kbn.valueFormats.celsius = function(value, decimals) { return kbn.toFixed(value, decimals) + ' °C'; }; + kbn.valueFormats.farenheit = function(value, decimals) { return kbn.toFixed(value, decimals) + ' °F'; }; + kbn.valueFormats.humidity = function(value, decimals) { return kbn.toFixed(value, decimals) + ' %H'; }; + kbn.valueFormats.ppm = function(value, decimals) { return kbn.toFixed(value, decimals) + ' ppm'; }; + kbn.valueFormats.velocityms = function(value, decimals) { return kbn.toFixed(value, decimals) + ' m/s'; }; + kbn.valueFormats.velocitykmh = function(value, decimals) { return kbn.toFixed(value, decimals) + ' km/h'; }; + kbn.valueFormats.velocitymph = function(value, decimals) { return kbn.toFixed(value, decimals) + ' mph'; }; + kbn.valueFormats.velocityknot = function(value, decimals) { return kbn.toFixed(value, decimals) + ' kn'; }; kbn.valueFormats.ms = function(size, decimals, scaledDecimals) { if (size === null) { return ""; } @@ -498,6 +509,7 @@ function($, _, moment) { {text: 'none' , value: 'none'}, {text: 'short', value: 'short'}, {text: 'percent', value: 'percent'}, + {text: 'ppm', value: 'ppm'}, ] }, { @@ -514,8 +526,8 @@ function($, _, moment) { submenu: [ {text: 'bits', value: 'bits'}, {text: 'bytes', value: 'bytes'}, - {text: 'kilo bytes', value: 'kbytes'}, - {text: 'mega bytes', value: 'mbytes'}, + {text: 'kilobytes', value: 'kbytes'}, + {text: 'megabytes', value: 'mbytes'}, ] }, { @@ -528,9 +540,29 @@ function($, _, moment) { { text: 'energy', submenu: [ - {text: 'watt', value: 'watt'}, - {text: 'joule', value: 'joule'}, - {text: 'eV', value: 'ev'}, + {text: 'watt (W)', value: 'watt'}, + {text: 'kilowatt (kW)', value: 'kwatt'}, + {text: 'watt-hour (Wh)', value: 'watth'}, + {text: 'kilowatt-hour (kWh)', value: 'kwatth'}, + {text: 'joule (J)', value: 'joule'}, + {text: 'electron volt (eV)', value: 'ev'}, + ] + }, + { + text: 'weather', + submenu: [ + {text: 'Celcius (°C)', value: 'celsius' }, + {text: 'Farenheit (°F)', value: 'farenheit'}, + {text: 'Humidity (%H)', value: 'humidity' }, + ] + }, + { + text: 'velocity', + submenu: [ + {text: 'm/s', value: 'velocityms' }, + {text: 'km/h', value: 'velocitykmh' }, + {text: 'mph', value: 'velocitymph' }, + {text: 'knot (kn)', value: 'velocityknot' }, ] }, ]; diff --git a/src/app/panels/graph/axisEditor.html b/src/app/panels/graph/axisEditor.html index 34f624e28e4..c23daa77711 100644 --- a/src/app/panels/graph/axisEditor.html +++ b/src/app/panels/graph/axisEditor.html @@ -9,7 +9,7 @@
  • Unit
  • -
  • Unit
  • -
    -
    diff --git a/src/app/features/dashboard/dashboardCtrl.js b/src/app/features/dashboard/dashboardCtrl.js index facd5fada43..8ec13b24f2d 100644 --- a/src/app/features/dashboard/dashboardCtrl.js +++ b/src/app/features/dashboard/dashboardCtrl.js @@ -72,6 +72,10 @@ function (angular, $, config) { $scope.contextSrv.lightTheme = $scope.dashboard.style === 'light'; }; + $scope.broadcastRefresh = function() { + $rootScope.$broadcast('refresh'); + }; + $scope.add_row = function(dash, row) { dash.rows.push(row); }; diff --git a/src/app/features/dashboard/dashboardSrv.js b/src/app/features/dashboard/dashboardSrv.js index 8a41ddee5d8..ef33c211d04 100644 --- a/src/app/features/dashboard/dashboardSrv.js +++ b/src/app/features/dashboard/dashboardSrv.js @@ -10,7 +10,7 @@ function (angular, $, kbn, _, moment) { var module = angular.module('grafana.services'); - module.factory('dashboardSrv', function($rootScope) { + module.factory('dashboardSrv', function() { function DashboardModel (data) { @@ -132,10 +132,6 @@ function (angular, $, kbn, _, moment) { moment.utc(date).format(format); }; - p.emit_refresh = function() { - $rootScope.$broadcast('refresh'); - }; - p._updateSchema = function(old) { var i, j, k; var oldVersion = this.version; diff --git a/src/app/features/dashboard/keybindings.js b/src/app/features/dashboard/keybindings.js index 588155ee16c..d558b554ebf 100644 --- a/src/app/features/dashboard/keybindings.js +++ b/src/app/features/dashboard/keybindings.js @@ -40,7 +40,7 @@ function(angular, $) { keyboardManager.bind('ctrl+o', function() { var current = scope.dashboard.sharedCrosshair; scope.dashboard.sharedCrosshair = !current; - scope.dashboard.emit_refresh('refresh'); + scope.broadcastRefresh(); }, { inputDisabled: true }); keyboardManager.bind('ctrl+l', function() { @@ -57,7 +57,7 @@ function(angular, $) { }, { inputDisabled: true }); keyboardManager.bind('ctrl+r', function() { - scope.dashboard.emit_refresh(); + scope.broadcastRefresh(); }, { inputDisabled: true }); keyboardManager.bind('ctrl+z', function(evt) { diff --git a/src/app/features/dashboard/viewStateSrv.js b/src/app/features/dashboard/viewStateSrv.js index 289701c05fb..08f651f8d5b 100644 --- a/src/app/features/dashboard/viewStateSrv.js +++ b/src/app/features/dashboard/viewStateSrv.js @@ -115,7 +115,7 @@ function (angular, _, $) { $timeout(function() { if (self.oldTimeRange !== self.fullscreenPanel.range) { - self.$scope.dashboard.emit_refresh(); + self.$scope.broadcastRefresh(); } else { self.fullscreenPanel.$broadcast('render'); From 7e82626b170b89fab7aea0056f536ae272c3ccfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 22 Feb 2015 08:25:44 +0100 Subject: [PATCH 088/118] Fixed bug with custom time range not being initialized correctly, Fixes #1427 --- src/vendor/angular/angular-strap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vendor/angular/angular-strap.js b/src/vendor/angular/angular-strap.js index 772b5162943..58c49c6b279 100644 --- a/src/vendor/angular/angular-strap.js +++ b/src/vendor/angular/angular-strap.js @@ -79,7 +79,7 @@ angular.module('$strap.directives').directive('bsDatepicker', [ var language = options.language || 'en', readFormat = attrs.dateFormat || options.format || $.fn.datepicker.dates[language] && $.fn.datepicker.dates[language].format || 'mm/dd/yyyy', format = isAppleTouch ? 'yyyy-mm-dd' : readFormat, dateFormatRegexp = regexpForDateFormat(format, language); if (controller) { controller.$formatters.unshift(function (modelValue) { - return type === 'date' && angular.isString(modelValue) && modelValue ? $.fn.datepicker.DPGlobal.parseDate(modelValue, $.fn.datepicker.DPGlobal.parseFormat(readFormat), language) : modelValue; + return type === 'date' && angular.isString(modelValue) && modelValue ? $.fn.datepicker.DPGlobal.parseDate(new Date(modelValue), $.fn.datepicker.DPGlobal.parseFormat(readFormat), language) : modelValue; }); controller.$parsers.unshift(function (viewValue) { if (!viewValue) { From fea6b970e59f442e88e6b9700d987e7e2f38fe88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 22 Feb 2015 08:44:37 +0100 Subject: [PATCH 089/118] Some prep work for using today as relative time, #1186 --- src/app/components/kbn.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/app/components/kbn.js b/src/app/components/kbn.js index 398fede6350..5ccd5b9508b 100644 --- a/src/app/components/kbn.js +++ b/src/app/components/kbn.js @@ -168,14 +168,22 @@ function($, _, moment) { if(_.isDate(text)) { return text; } - var time, - mathString = "", - index, - parseString; + + var time; + var mathString = ""; + var index; + var parseString; + if (text.substring(0,3) === "now") { time = new Date(); - mathString = text.substring("now".length); - } else { + mathString = text.substring(3); + } + else if (text.substring(0,5) === 'today') { + time = new Date(); + time.setHours(0,0,0,0); + mathString = text.substring(5); + } + else { index = text.indexOf("||"); parseString; if (index === -1) { From a146a24c0625571b997d96cff70f4d407ad0f152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 Feb 2015 09:54:26 +0100 Subject: [PATCH 090/118] Fixed small issue in search and showing Home dashboard when filtering by tag --- src/app/controllers/search.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/controllers/search.js b/src/app/controllers/search.js index e724aadcf7f..767b1e5d88b 100644 --- a/src/app/controllers/search.js +++ b/src/app/controllers/search.js @@ -14,7 +14,7 @@ function (angular, _, config) { $scope.giveSearchFocus = 0; $scope.selectedIndex = -1; $scope.results = {dashboards: [], tags: [], metrics: []}; - $scope.query = { query: '' }; + $scope.query = { query: '', tag: '', starred: false }; $scope.db = datasourceSrv.getGrafanaDB(); $scope.currentSearchId = 0; @@ -72,12 +72,17 @@ function (angular, _, config) { return dash; }); - if ($scope.query.query === "" && !$scope.query.starred) { + if ($scope.queryHasNoFilters()) { $scope.results.dashboards.unshift({ title: 'Home', url: config.appSubUrl + '/', isHome: true }); } }); }; + $scope.queryHasNoFilters = function() { + var query = $scope.query; + return query.query === '' && query.starred === false && query.tag === ''; + }; + $scope.filterByTag = function(tag, evt) { $scope.query.tag = tag; $scope.query.tagcloud = false; From 4a93d205bbf4ffc21ecb9f95f26f00b9190308dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 Feb 2015 10:13:16 +0100 Subject: [PATCH 091/118] Moved dashboard import and json import to same view, found via dashboard search dropdown, #1511 --- src/app/features/account/all.js | 1 - .../features/account/partials/account.html | 1 - src/app/features/dashboard/all.js | 1 + .../{account => dashboard}/importCtrl.js | 2 +- .../partials/import.html | 23 +++++++++++++++---- src/app/partials/search.html | 15 ++---------- src/app/routes/backend/all.js | 8 +++---- src/css/less/search.less | 2 +- 8 files changed, 27 insertions(+), 26 deletions(-) rename src/app/features/{account => dashboard}/importCtrl.js (96%) rename src/app/features/{account => dashboard}/partials/import.html (72%) diff --git a/src/app/features/account/all.js b/src/app/features/account/all.js index 277ae1a75f1..b2c7b2962e9 100644 --- a/src/app/features/account/all.js +++ b/src/app/features/account/all.js @@ -3,6 +3,5 @@ define([ './datasourcesCtrl', './datasourceEditCtrl', './apiKeysCtrl', - './importCtrl', './accountCtrl', ], function () {}); diff --git a/src/app/features/account/partials/account.html b/src/app/features/account/partials/account.html index a25a6bb7bc3..012ef18d6f2 100644 --- a/src/app/features/account/partials/account.html +++ b/src/app/features/account/partials/account.html @@ -2,7 +2,6 @@ diff --git a/src/app/features/dashboard/all.js b/src/app/features/dashboard/all.js index a64800867c2..cadab505e5b 100644 --- a/src/app/features/dashboard/all.js +++ b/src/app/features/dashboard/all.js @@ -13,4 +13,5 @@ define([ './timeSrv', './unsavedChangesSrv', './directives/dashSearchView', + './importCtrl', ], function () {}); diff --git a/src/app/features/account/importCtrl.js b/src/app/features/dashboard/importCtrl.js similarity index 96% rename from src/app/features/account/importCtrl.js rename to src/app/features/dashboard/importCtrl.js index f14374d2db1..8b3612ea70e 100644 --- a/src/app/features/account/importCtrl.js +++ b/src/app/features/dashboard/importCtrl.js @@ -7,7 +7,7 @@ function (angular, _) { var module = angular.module('grafana.controllers'); - module.controller('ImportCtrl', function($scope, $http, backendSrv, datasourceSrv) { + module.controller('DashboardImportCtrl', function($scope, $http, backendSrv, datasourceSrv) { $scope.init = function() { $scope.datasources = []; diff --git a/src/app/features/account/partials/import.html b/src/app/features/dashboard/partials/import.html similarity index 72% rename from src/app/features/account/partials/import.html rename to src/app/features/dashboard/partials/import.html index 39ec78f0032..965331bdd15 100644 --- a/src/app/features/account/partials/import.html +++ b/src/app/features/dashboard/partials/import.html @@ -1,14 +1,27 @@ - +
    -

    Import dashboards

    +

    + Import dashboard + Load dashboard JSON layout from file +

    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +

    Migrate dashboards

      diff --git a/src/app/partials/search.html b/src/app/partials/search.html index 61c06229b04..8320c9d19b7 100644 --- a/src/app/partials/search.html +++ b/src/app/partials/search.html @@ -62,26 +62,15 @@
    -
    -
    -
    -
    Local File Load dashboard JSON layout from file
    -
    -
    -
    -
    -
    -
    -
    - + + +
    + +
    + + + + + + +
    {{dash.name}} +
    + +
    +
    diff --git a/src/app/features/dashboard/partials/import.html b/src/app/features/dashboard/partials/import.html index 965331bdd15..af02069657a 100644 --- a/src/app/features/dashboard/partials/import.html +++ b/src/app/features/dashboard/partials/import.html @@ -7,7 +7,7 @@

    - Import dashboard + Import file Load dashboard JSON layout from file

    @@ -23,7 +23,7 @@

    Migrate dashboards

    -
    +
    • Dashboard source @@ -62,6 +62,9 @@
    + +
    +
    diff --git a/src/app/partials/dasheditor.html b/src/app/partials/dasheditor.html index 843f73b24db..2dc2b18e523 100644 --- a/src/app/partials/dasheditor.html +++ b/src/app/partials/dasheditor.html @@ -5,7 +5,7 @@
    -
    +
    @@ -70,11 +70,7 @@
    -
    - -
    - -
    +
    diff --git a/src/app/partials/import.html b/src/app/partials/import.html deleted file mode 100644 index e89aff41342..00000000000 --- a/src/app/partials/import.html +++ /dev/null @@ -1,35 +0,0 @@ -
    -
    Import dashboards from graphite web
    - -
    -
    -
    - - - -
    - -
    -
    - -
    - - - - - -
    {{dash.name}} - - import - -
    -
    - -
    - {{error}} -
    -
    From 1a106e5c3881a5e0553fb88315dc0959f6436bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 Feb 2015 11:24:22 +0100 Subject: [PATCH 093/118] Added change password ability to admin > edit user view, #1446 --- pkg/api/admin_users.go | 31 +++++++++++++++++ pkg/api/api.go | 3 +- pkg/api/dtos/user.go | 4 +++ src/app/features/admin/adminEditUserCtrl.js | 33 ++++++++++++------- .../features/admin/partials/edit_user.html | 31 ++++++++++------- src/app/features/admin/partials/users.html | 2 +- src/app/routes/backend/all.js | 2 +- 7 files changed, 79 insertions(+), 27 deletions(-) diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index 9402eb41b20..ec83aa47df1 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -5,6 +5,7 @@ import ( "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 AdminSearchUsers(c *middleware.Context) { @@ -91,6 +92,36 @@ func AdminUpdateUser(c *middleware.Context, form dtos.AdminUpdateUserForm) { c.JsonOK("User updated") } +func AdminUpdateUserPassword(c *middleware.Context, form dtos.AdminUpdateUserPasswordForm) { + userId := c.ParamsInt64(":id") + + if len(form.Password) < 4 { + c.JsonApiErr(400, "New password too short", nil) + return + } + + userQuery := m.GetUserByIdQuery{Id: userId} + + if err := bus.Dispatch(&userQuery); err != nil { + c.JsonApiErr(500, "Could not read user from database", err) + return + } + + passwordHashed := util.EncodePassword(form.Password, userQuery.Result.Salt) + + cmd := m.ChangeUserPasswordCommand{ + UserId: userId, + NewPassword: passwordHashed, + } + + if err := bus.Dispatch(&cmd); err != nil { + c.JsonApiErr(500, "Failed to update user password", err) + return + } + + c.JsonOK("User password updated") +} + func AdminDeleteUser(c *middleware.Context) { userId := c.ParamsInt64(":id") diff --git a/pkg/api/api.go b/pkg/api/api.go index c6d050547ef..84fee61005f 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -102,7 +102,8 @@ func Register(r *macaron.Macaron) { r.Get("/users", AdminSearchUsers) r.Get("/users/:id", AdminGetUser) r.Post("/users", bind(dtos.AdminCreateUserForm{}), AdminCreateUser) - r.Put("/users/:id", bind(dtos.AdminUpdateUserForm{}), AdminUpdateUser) + r.Put("/users/:id/details", bind(dtos.AdminUpdateUserForm{}), AdminUpdateUser) + r.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword) r.Delete("/users/:id", AdminDeleteUser) }, reqGrafanaAdmin) diff --git a/pkg/api/dtos/user.go b/pkg/api/dtos/user.go index 69fc3797029..3d671e64654 100644 --- a/pkg/api/dtos/user.go +++ b/pkg/api/dtos/user.go @@ -12,3 +12,7 @@ type AdminUpdateUserForm struct { Login string `json:"login"` Name string `json:"name"` } + +type AdminUpdateUserPasswordForm struct { + Password string `json:"password" binding:"Required"` +} diff --git a/src/app/features/admin/adminEditUserCtrl.js b/src/app/features/admin/adminEditUserCtrl.js index 86b59c4599b..0d473bd950e 100644 --- a/src/app/features/admin/adminEditUserCtrl.js +++ b/src/app/features/admin/adminEditUserCtrl.js @@ -11,10 +11,7 @@ function (angular) { $scope.init = function() { if ($routeParams.id) { - $scope.createMode = false; $scope.getUser($routeParams.id); - } else { - $scope.createMode = true; } }; @@ -25,17 +22,29 @@ function (angular) { }); }; + $scope.setPassword = function () { + if (!$scope.passwordForm.$valid) { return; } + + var payload = { password: $scope.password }; + backendSrv.put('/api/admin/users/' + $scope.user_id + '/password', payload).then(function() { + $location.path('/admin/users'); + }); + }; + + $scope.create = function() { + if (!$scope.userForm.$valid) { return; } + + backendSrv.post('/api/admin/users', $scope.user).then(function() { + $location.path('/admin/users'); + }); + }; + $scope.update = function() { if (!$scope.userForm.$valid) { return; } - if ($scope.createMode) { - backendSrv.post('/api/admin/users', $scope.user).then(function() { - $location.path('/admin/users'); - }); - } else { - backendSrv.put('/api/admin/users/' + $scope.user_id, $scope.user).then(function() { - $location.path('/admin/users'); - }); - } + + backendSrv.put('/api/admin/users/' + $scope.user_id + '/details', $scope.user).then(function() { + $location.path('/admin/users'); + }); }; $scope.init(); diff --git a/src/app/features/admin/partials/edit_user.html b/src/app/features/admin/partials/edit_user.html index ecc03fd1aad..bc060347b86 100644 --- a/src/app/features/admin/partials/edit_user.html +++ b/src/app/features/admin/partials/edit_user.html @@ -2,18 +2,14 @@
    -

    - Create a new user -

    - -

    +

    Edit user

    @@ -52,14 +48,25 @@
    +
    -
    +
    + + + +

    + Change password +

    + +
    +
    +
    • - Password + New password
    • - +
    @@ -67,8 +74,8 @@

    - - +
    +
    diff --git a/src/app/features/admin/partials/users.html b/src/app/features/admin/partials/users.html index fb86b18eacb..a27d755ba9d 100644 --- a/src/app/features/admin/partials/users.html +++ b/src/app/features/admin/partials/users.html @@ -18,7 +18,7 @@ Name Login Email - Admin + Grafana Admin diff --git a/src/app/routes/backend/all.js b/src/app/routes/backend/all.js index 29e6c3df623..e570603d808 100644 --- a/src/app/routes/backend/all.js +++ b/src/app/routes/backend/all.js @@ -75,7 +75,7 @@ define([ controller : 'AdminUsersCtrl', }) .when('/admin/users/create', { - templateUrl: 'app/features/admin/partials/edit_user.html', + templateUrl: 'app/features/admin/partials/new_user.html', controller : 'AdminEditUserCtrl', }) .when('/admin/users/edit/:id', { From 12e28c1895abd3efdc3b32b99475c9e76770a185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 Feb 2015 11:24:45 +0100 Subject: [PATCH 094/118] added missing file --- src/app/features/admin/partials/new_user.html | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/app/features/admin/partials/new_user.html diff --git a/src/app/features/admin/partials/new_user.html b/src/app/features/admin/partials/new_user.html new file mode 100644 index 00000000000..4141fd9884a --- /dev/null +++ b/src/app/features/admin/partials/new_user.html @@ -0,0 +1,67 @@ + + + + +
    +
    +

    + Create a new user +

    + +
    +
    +
    +
      +
    • + Name +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + Email +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + Username +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + Password +
    • +
    • + +
    • +
    +
    +
    +
    + +
    + +
    +
    +
    From 138e720695a1c16e6964c77ede261f7487ac8837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 Feb 2015 11:30:58 +0100 Subject: [PATCH 095/118] Increased max user list to 1000 for user admin page, need to paging, but will have to wait a few days --- pkg/api/admin_users.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index ec83aa47df1..07a8cf05fd4 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -9,7 +9,7 @@ import ( ) func AdminSearchUsers(c *middleware.Context) { - query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 20} + query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000} if err := bus.Dispatch(&query); err != nil { c.JsonApiErr(500, "Failed to fetch users", err) return From 65307c463a5ffc25b8e6923c122c83f2792979ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 Feb 2015 12:48:07 +0100 Subject: [PATCH 096/118] Refactoring opentsdb editor view, merging #1438, made Downsampling enabled by default --- CHANGELOG.md | 5 + src/app/features/opentsdb/datasource.js | 4 +- .../opentsdb/partials/query.editor.html | 168 +++++++++++------- 3 files changed, 108 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 330aad603f3..b2c3f4e495c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 2.0.0 (unreleased) +**Changes** +- [Issue #1438](https://github.com/grafana/grafana/issues/1438). OpenTSDB: Automatic downsample interval passed to OpenTSDB (depends on timespan and graph width) +- NOTICE, Downsampling is now enabled by default, so if you have not picked a downsample aggregator in your metric query do so or your graphs will be missleading +- This will make Grafana a lot quicker for OpenTSDB users when viewing large time spans without having to change the downsample interval manually. + **New features** - [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/opentsdb/datasource.js b/src/app/features/opentsdb/datasource.js index a90379b9fc4..17b98ba9dca 100644 --- a/src/app/features/opentsdb/datasource.js +++ b/src/app/features/opentsdb/datasource.js @@ -25,12 +25,14 @@ function (angular, _, kbn) { var start = convertToTSDBTime(options.range.from); var end = convertToTSDBTime(options.range.to); var qs = []; + if (options.interval.match(/\.[0-9]+s/)) { options.interval = parseFloat(options.interval)*1000 + "ms"; } _.each(options.targets, function(target) { qs.push(convertTargetToQuery(target, options.interval)); }); + var queries = _.compact(qs); // No valid targets, return the empty result to save a round trip. @@ -155,7 +157,7 @@ function (angular, _, kbn) { } } - if (target.shouldDownsample) { + if (!target.disableDownsampling) { var buf = target.downsampleInterval || interval; query.downsample = templateSrv.replace(buf) + "-" + target.downsampleAggregator; } diff --git a/src/app/features/opentsdb/partials/query.editor.html b/src/app/features/opentsdb/partials/query.editor.html index 794d148676c..09def6a471b 100644 --- a/src/app/features/opentsdb/partials/query.editor.html +++ b/src/app/features/opentsdb/partials/query.editor.html @@ -43,14 +43,18 @@
    -
    + +
    + +
    + + +
    +
    +
    From 2c5f19253cc1f2df98aa57169006b0f84ea766fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 Feb 2015 12:56:40 +0100 Subject: [PATCH 097/118] Added margin between opentsdb queries --- src/app/features/opentsdb/partials/query.editor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/features/opentsdb/partials/query.editor.html b/src/app/features/opentsdb/partials/query.editor.html index 09def6a471b..b732d999719 100644 --- a/src/app/features/opentsdb/partials/query.editor.html +++ b/src/app/features/opentsdb/partials/query.editor.html @@ -1,7 +1,7 @@
    From a5fd40ed8045e4072f58ded23e811204285482b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 Feb 2015 13:04:32 +0100 Subject: [PATCH 098/118] OpenTSDB: Fixed issue with auto interval variable going below 1s, Fixes #1325 --- src/app/features/opentsdb/datasource.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/app/features/opentsdb/datasource.js b/src/app/features/opentsdb/datasource.js index 17b98ba9dca..4dc639ce45f 100644 --- a/src/app/features/opentsdb/datasource.js +++ b/src/app/features/opentsdb/datasource.js @@ -26,9 +26,6 @@ function (angular, _, kbn) { var end = convertToTSDBTime(options.range.to); var qs = []; - if (options.interval.match(/\.[0-9]+s/)) { - options.interval = parseFloat(options.interval)*1000 + "ms"; - } _.each(options.targets, function(target) { qs.push(convertTargetToQuery(target, options.interval)); }); @@ -158,8 +155,13 @@ function (angular, _, kbn) { } if (!target.disableDownsampling) { - var buf = target.downsampleInterval || interval; - query.downsample = templateSrv.replace(buf) + "-" + target.downsampleAggregator; + interval = templateSrv.replace(target.downsampleInterval || interval); + + if (interval.match(/\.[0-9]+s/)) { + interval = parseFloat(interval)*1000 + "ms"; + } + + query.downsample = interval + "-" + target.downsampleAggregator; } query.tags = angular.copy(target.tags); From a8c90e2365d056725e7d1c93162f6bf282779ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 Feb 2015 14:20:24 +0100 Subject: [PATCH 099/118] Small update to commands --- pkg/cmd/datasource.go | 4 ++-- pkg/cmd/web.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/datasource.go b/pkg/cmd/datasource.go index 660e59a9702..ea59a0599ea 100644 --- a/pkg/cmd/datasource.go +++ b/pkg/cmd/datasource.go @@ -54,13 +54,13 @@ var ( }, } DescribeDataSource = cli.Command{ - Name: "datasource:info", + Name: "datasources:info", Usage: "describe the details of a datasource", Description: "Describes the details of a datasource", Action: describeDataSource, } DeleteDataSource = cli.Command{ - Name: "datasource:delete", + Name: "datasources:delete", Usage: "Deletes a datasource", Description: "Deletes a datasource", Action: deleteDataSource, diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index b858072398c..ad3cd28f1af 100644 --- a/pkg/cmd/web.go +++ b/pkg/cmd/web.go @@ -23,7 +23,7 @@ import ( var Web = cli.Command{ Name: "web", - Usage: "grafana web", + Usage: "Starts Grafana backend & web server", Description: "Starts Grafana backend & web server", Action: runWeb, } From 5b2715515fb791c92827cb4fa8d75767e130f412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 Feb 2015 16:39:52 +0100 Subject: [PATCH 100/118] Began work on Account -> Organization rethink, #1506 --- pkg/api/dtos/models.go | 2 +- src/app/controllers/sidemenuCtrl.js | 54 ++----------------- .../features/account/partials/account.html | 4 +- .../account/partials/datasourceEdit.html | 2 +- .../account/partials/datasources.html | 2 +- src/app/features/account/partials/users.html | 2 +- .../features/admin/partials/edit_user.html | 2 +- src/app/features/admin/partials/new_user.html | 2 +- src/app/features/admin/partials/settings.html | 2 +- src/app/features/admin/partials/users.html | 2 +- src/app/partials/sidemenu.html | 28 ++++++---- src/css/less/sidemenu.less | 35 +++++++++--- 12 files changed, 61 insertions(+), 76 deletions(-) diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 12d72314f8d..1a05da32004 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -20,7 +20,7 @@ type CurrentUser struct { Email string `json:"email"` Name string `json:"name"` AccountRole m.RoleType `json:"accountRole"` - AccountName string `json:"acountName"` + AccountName string `json:"accountName"` IsGrafanaAdmin bool `json:"isGrafanaAdmin"` GravatarUrl string `json:"gravatarUrl"` } diff --git a/src/app/controllers/sidemenuCtrl.js b/src/app/controllers/sidemenuCtrl.js index 184e2cab2fb..df41138a0ae 100644 --- a/src/app/controllers/sidemenuCtrl.js +++ b/src/app/controllers/sidemenuCtrl.js @@ -18,69 +18,23 @@ function (angular, _, $, config) { $scope.menu = []; $scope.menu.push({ text: "Dashboards", - icon: "fa fa-th-large", + icon: "fa fa-fw fa-th-large", href: $scope.getUrl("/"), }); if (contextSrv.hasRole('Admin')) { $scope.menu.push({ text: "Data Sources", - icon: "fa fa-database", + icon: "fa fa-fw fa-database", href: $scope.getUrl("/account/datasources"), }); $scope.menu.push({ - text: "Account", href: $scope.getUrl("/account"), - requireRole: "Admin", - icon: "fa fa-shield", - }); - } - - if (contextSrv.user.isGrafanaAdmin) { - $scope.menu.push({ - text: "Admin", href: $scope.getUrl("/admin/settings"), - icon: "fa fa-cube", - requireSignedIn: true, + text: "Organization", href: $scope.getUrl("/account"), + icon: "fa fa-fw fa-users", }); } $scope.updateState = function() { - var currentPath = config.appSubUrl + $location.path(); - var search = $location.search(); - - _.each($scope.menu, function(item) { - item.active = false; - - if (item.href === currentPath) { - item.active = true; - } - - if (item.startsWith) { - if (currentPath.indexOf(item.startsWith) === 0) { - item.active = true; - item.href = currentPath; - } - } - - _.each(item.links, function(link) { - link.active = false; - - if (link.editview) { - var params = {}; - _.each(search, function(value, key) { - if (value !== null) { params[key] = value; } - }); - - params.editview = link.editview; - link.href = currentPath + '?' + $.param(params); - } - - if (link.href === currentPath) { - item.active = true; - link.active = true; - } - }); - }); - }; $scope.init = function() { diff --git a/src/app/features/account/partials/account.html b/src/app/features/account/partials/account.html index 012ef18d6f2..a8f01edaf6a 100644 --- a/src/app/features/account/partials/account.html +++ b/src/app/features/account/partials/account.html @@ -1,4 +1,4 @@ - +