Просмотр исходного кода

Merge branch 'master' into docs-2.0

Torkel Ödegaard 10 лет назад
Родитель
Сommit
75e6947c5a
68 измененных файлов с 628 добавлено и 260 удалено
  1. 21 1
      CHANGELOG.md
  2. 9 9
      Godeps/Godeps.json
  3. 3 7
      Godeps/_workspace/src/github.com/dalu/slug/README.md
  4. 0 0
      Godeps/_workspace/src/github.com/dalu/slug/default_substitution.go
  5. 1 5
      Godeps/_workspace/src/github.com/dalu/slug/doc.go
  6. 0 0
      Godeps/_workspace/src/github.com/dalu/slug/languages_substitution.go
  7. 1 1
      Godeps/_workspace/src/github.com/dalu/slug/slug.go
  8. 0 0
      Godeps/_workspace/src/github.com/dalu/slug/slug_test.go
  9. 0 0
      Godeps/_workspace/src/github.com/dalu/unidecode/.gitignore
  10. 0 0
      Godeps/_workspace/src/github.com/dalu/unidecode/LICENSE
  11. 0 6
      Godeps/_workspace/src/github.com/dalu/unidecode/README.md
  12. 0 0
      Godeps/_workspace/src/github.com/dalu/unidecode/decode.go
  13. 0 0
      Godeps/_workspace/src/github.com/dalu/unidecode/make_table.go
  14. 0 0
      Godeps/_workspace/src/github.com/dalu/unidecode/table.go
  15. 0 0
      Godeps/_workspace/src/github.com/dalu/unidecode/table.txt
  16. 0 0
      Godeps/_workspace/src/github.com/dalu/unidecode/unidecode.go
  17. 0 0
      Godeps/_workspace/src/github.com/dalu/unidecode/unidecode_test.go
  18. 0 2
      Godeps/_workspace/src/github.com/gosimple/slug/.gitignore
  19. 0 24
      Godeps/_workspace/src/gopkgs.com/unidecode.v1/gopkgs.go
  20. 4 2
      Gruntfile.js
  21. 1 1
      README.md
  22. 28 10
      build.go
  23. 4 2
      conf/defaults.ini
  24. 5 6
      conf/sample.ini
  25. 1 1
      docs/Makefile
  26. 4 4
      docs/sources/index.md
  27. 18 4
      docs/sources/installation/configuration.md
  28. 2 2
      docs/sources/installation/debian.md
  29. 4 4
      docs/sources/installation/rpm.md
  30. 17 2
      docs/sources/installation/windows.md
  31. 143 1
      docs/sources/reference/http_api.md
  32. 1 1
      docs/sources/reference/timerange.md
  33. 1 1
      latest.json
  34. 1 1
      package.json
  35. 5 1
      packaging/deb/init.d/grafana-server
  36. 0 2
      packaging/rpm/init.d/grafana-server
  37. 5 4
      pkg/api/dtos/models.go
  38. 1 0
      pkg/api/index.go
  39. 7 2
      pkg/api/login_oauth.go
  40. 1 1
      pkg/models/dashboards.go
  41. 6 0
      pkg/services/sqlstore/migrations/dashboard_mig.go
  42. 6 0
      pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go
  43. 10 2
      pkg/services/sqlstore/migrator/migrations.go
  44. 60 8
      pkg/social/social.go
  45. 6 0
      public/app/components/kbn.js
  46. 8 1
      public/app/controllers/loginCtrl.js
  47. 6 0
      public/app/features/dashboard/dashboardNavCtrl.js
  48. 10 12
      public/app/features/dashboard/dashboardSrv.js
  49. 6 6
      public/app/features/dashboard/partials/dashboardTopNav.html
  50. 1 1
      public/app/features/dashboard/unsavedChangesSrv.js
  51. 1 1
      public/app/features/panel/panelHelper.js
  52. 8 0
      public/app/features/panel/panelSrv.js
  53. 21 8
      public/app/features/templating/templateValuesSrv.js
  54. 2 3
      public/app/panels/graph/axisEditor.html
  55. 4 3
      public/app/panels/graph/graph.js
  56. 3 3
      public/app/panels/graph/module.js
  57. 2 2
      public/app/partials/dashboard.html
  58. 100 45
      public/app/partials/dasheditor.html
  59. 1 1
      public/app/plugins/datasource/elasticsearch/datasource.js
  60. 2 3
      public/app/plugins/datasource/grafana/partials/query.editor.html
  61. 38 18
      public/app/routes/dashLoadControllers.js
  62. 2 1
      public/app/services/backendSrv.js
  63. 7 8
      public/app/services/contextSrv.js
  64. 8 22
      public/css/less/bootstrap-tagsinput.less
  65. 2 1
      public/css/less/search.less
  66. 18 2
      public/test/specs/dashboardSrv-specs.js
  67. 1 1
      public/vendor/tagsinput/bootstrap-tagsinput.js
  68. 1 1
      tasks/options/compress.js

+ 21 - 1
CHANGELOG.md

@@ -1,4 +1,24 @@
-# 2.0.0 (2015-04-20)
+# 2.1.0 (unreleased - master branch)
+
+**Backend**
+- [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski
+
+
+# 2.0.3 (unreleased - 2.0.x branch)
+
+**Fixes**
+- [Issue #1872](https://github.com/grafana/grafana/issues/1872). Firefox/IE issue, invisible text in dashboard search fixed
+- [Issue #1857](https://github.com/grafana/grafana/issues/1857). /api/login/ping Fix for issue when behind reverse proxy and subpath
+- [Issue #1863](https://github.com/grafana/grafana/issues/1863). MySQL: Dashboard.data column type changed to mediumtext (sql migration added)
+
+# 2.0.2 (2015-04-22)
+
+**Fixes**
+- [Issue #1832](https://github.com/grafana/grafana/issues/1832). Graph Panel + Legend Table mode: Many series casued zero height graph, now legend will never reduce the height of the graph below 50% of row height.
+- [Issue #1846](https://github.com/grafana/grafana/issues/1846). Snapshots: Fixed issue with snapshoting dashboards with an interval template variable
+- [Issue #1848](https://github.com/grafana/grafana/issues/1848). Panel timeshift: You can now use panel timeshift without a relative time override
+
+# 2.0.1 (2015-04-20)
 
 
 **Fixes**
 **Fixes**
 - [Issue #1784](https://github.com/grafana/grafana/issues/1784). Data source proxy: Fixed issue with using data source proxy when grafana is behind nginx suburl
 - [Issue #1784](https://github.com/grafana/grafana/issues/1784). Data source proxy: Fixed issue with using data source proxy when grafana is behind nginx suburl

+ 9 - 9
Godeps/Godeps.json

@@ -1,6 +1,6 @@
 {
 {
 	"ImportPath": "github.com/grafana/grafana",
 	"ImportPath": "github.com/grafana/grafana",
-	"GoVersion": "go1.4.2",
+	"GoVersion": "go1.3",
 	"Packages": [
 	"Packages": [
 		"./pkg/..."
 		"./pkg/..."
 	],
 	],
@@ -13,6 +13,14 @@
 			"ImportPath": "github.com/Unknwon/macaron",
 			"ImportPath": "github.com/Unknwon/macaron",
 			"Rev": "93de4f3fad97bf246b838f828e2348f46f21f20a"
 			"Rev": "93de4f3fad97bf246b838f828e2348f46f21f20a"
 		},
 		},
+		{
+			"ImportPath": "github.com/dalu/slug",
+			"Rev": "6dbd13912e9be466e2c1de349a2c7d1466c97e07"
+		},
+		{
+			"ImportPath": "github.com/dalu/unidecode",
+			"Rev": "339814d47f3e32a6f7036a0a4c56ed9b373dd755"
+		},
 		{
 		{
 			"ImportPath": "github.com/go-sql-driver/mysql",
 			"ImportPath": "github.com/go-sql-driver/mysql",
 			"Comment": "v1.2-26-g9543750",
 			"Comment": "v1.2-26-g9543750",
@@ -27,10 +35,6 @@
 			"Comment": "v0.4.2-58-ge2889e5",
 			"Comment": "v0.4.2-58-ge2889e5",
 			"Rev": "e2889e5517600b82905f1d2ba8b70deb71823ffe"
 			"Rev": "e2889e5517600b82905f1d2ba8b70deb71823ffe"
 		},
 		},
-		{
-			"ImportPath": "github.com/gosimple/slug",
-			"Rev": "a2392a4a87fa0366cbff131d3fd421f83f52492f"
-		},
 		{
 		{
 			"ImportPath": "github.com/jtolds/gls",
 			"ImportPath": "github.com/jtolds/gls",
 			"Rev": "f1ac7f4f24f50328e6bc838ca4437d1612a0243c"
 			"Rev": "f1ac7f4f24f50328e6bc838ca4437d1612a0243c"
@@ -87,10 +91,6 @@
 		{
 		{
 			"ImportPath": "gopkgs.com/pool.v1",
 			"ImportPath": "gopkgs.com/pool.v1",
 			"Rev": "c850f092aad1780cbffff25f471c5cc32097932a"
 			"Rev": "c850f092aad1780cbffff25f471c5cc32097932a"
-		},
-		{
-			"ImportPath": "gopkgs.com/unidecode.v1",
-			"Rev": "4deae2c05236b41cc39f8144ac87a837ba974d40"
 		}
 		}
 	]
 	]
 }
 }

+ 3 - 7
Godeps/_workspace/src/github.com/gosimple/slug/README.md → Godeps/_workspace/src/github.com/dalu/slug/README.md

@@ -4,10 +4,9 @@ slug
 Package `slug` generate slug from unicode string, URL-friendly slugify with
 Package `slug` generate slug from unicode string, URL-friendly slugify with
 multiple languages support.
 multiple languages support.
 
 
-[![GoDoc](https://godoc.org/github.com/gosimple/slug?status.png)](https://godoc.org/github.com/gosimple/slug)
-[![Build Status](https://drone.io/github.com/gosimple/slug/status.png)](https://drone.io/github.com/gosimple/slug/latest)
+[![GoDoc](https://godoc.org/github.com/dalu/slug?status.png)](https://godoc.org/github.com/dalu/slug)
 
 
-[Documentation online](http://godoc.org/github.com/gosimple/slug)
+[Documentation online](http://godoc.org/github.com/dalu/slug)
 
 
 ## Example
 ## Example
 
 
@@ -38,12 +37,9 @@ multiple languages support.
 		fmt.Println(textSub) // Will print 'sand-is-hot'
 		fmt.Println(textSub) // Will print 'sand-is-hot'
 	}
 	}
 
 
-### Requests or bugs?
-<https://github.com/gosimple/slug/issues>
-
 ## Installation
 ## Installation
 
 
-	go get -u github.com/gosimple/slug
+	go get -u github.com/dalu/slug
 
 
 ## License
 ## License
 
 

+ 0 - 0
Godeps/_workspace/src/github.com/gosimple/slug/default_substitution.go → Godeps/_workspace/src/github.com/dalu/slug/default_substitution.go


+ 1 - 5
Godeps/_workspace/src/github.com/gosimple/slug/doc.go → Godeps/_workspace/src/github.com/dalu/slug/doc.go

@@ -12,7 +12,7 @@ Example:
 	package main
 	package main
 
 
 	import(
 	import(
-		"github.com/gosimple/slug"
+		"github.com/dalu/slug"
 		"fmt"
 		"fmt"
 	)
 	)
 
 
@@ -35,9 +35,5 @@ Example:
 		textSub := slug.Make("water is hot")
 		textSub := slug.Make("water is hot")
 		fmt.Println(textSub) // Will print 'sand-is-hot'
 		fmt.Println(textSub) // Will print 'sand-is-hot'
 	}
 	}
-
-Requests or bugs?
-
-https://github.com/gosimple/slug/issues
 */
 */
 package slug
 package slug

+ 0 - 0
Godeps/_workspace/src/github.com/gosimple/slug/languages_substitution.go → Godeps/_workspace/src/github.com/dalu/slug/languages_substitution.go


+ 1 - 1
Godeps/_workspace/src/github.com/gosimple/slug/slug.go → Godeps/_workspace/src/github.com/dalu/slug/slug.go

@@ -6,7 +6,7 @@
 package slug
 package slug
 
 
 import (
 import (
-	"gopkgs.com/unidecode.v1"
+	"github.com/dalu/unidecode"
 	"regexp"
 	"regexp"
 	"strings"
 	"strings"
 )
 )

+ 0 - 0
Godeps/_workspace/src/github.com/gosimple/slug/slug_test.go → Godeps/_workspace/src/github.com/dalu/slug/slug_test.go


+ 0 - 0
Godeps/_workspace/src/gopkgs.com/unidecode.v1/.gitignore → Godeps/_workspace/src/github.com/dalu/unidecode/.gitignore


+ 0 - 0
Godeps/_workspace/src/gopkgs.com/unidecode.v1/LICENSE → Godeps/_workspace/src/github.com/dalu/unidecode/LICENSE


+ 0 - 6
Godeps/_workspace/src/gopkgs.com/unidecode.v1/README.md → Godeps/_workspace/src/github.com/dalu/unidecode/README.md

@@ -3,10 +3,4 @@ unidecode
 
 
 Unicode transliterator in Golang - Replaces non-ASCII characters with their ASCII approximations.
 Unicode transliterator in Golang - Replaces non-ASCII characters with their ASCII approximations.
 
 
-Please, use the following import path to ensure a stable API:
-
-```go
-    import "gopkgs.com/unidecode.v1"
-```
-
 View other available versions, documentation and examples at http://gopkgs.com/unidecode
 View other available versions, documentation and examples at http://gopkgs.com/unidecode

+ 0 - 0
Godeps/_workspace/src/gopkgs.com/unidecode.v1/decode.go → Godeps/_workspace/src/github.com/dalu/unidecode/decode.go


+ 0 - 0
Godeps/_workspace/src/gopkgs.com/unidecode.v1/make_table.go → Godeps/_workspace/src/github.com/dalu/unidecode/make_table.go


+ 0 - 0
Godeps/_workspace/src/gopkgs.com/unidecode.v1/table.go → Godeps/_workspace/src/github.com/dalu/unidecode/table.go


+ 0 - 0
Godeps/_workspace/src/gopkgs.com/unidecode.v1/table.txt → Godeps/_workspace/src/github.com/dalu/unidecode/table.txt


+ 0 - 0
Godeps/_workspace/src/gopkgs.com/unidecode.v1/unidecode.go → Godeps/_workspace/src/github.com/dalu/unidecode/unidecode.go


+ 0 - 0
Godeps/_workspace/src/gopkgs.com/unidecode.v1/unidecode_test.go → Godeps/_workspace/src/github.com/dalu/unidecode/unidecode_test.go


+ 0 - 2
Godeps/_workspace/src/github.com/gosimple/slug/.gitignore

@@ -1,2 +0,0 @@
-_*
-cover*.out

+ 0 - 24
Godeps/_workspace/src/gopkgs.com/unidecode.v1/gopkgs.go

@@ -1,24 +0,0 @@
-package unidecode
-
-import (
-	"fmt"
-	"reflect"
-)
-
-// gopkgs.go: v1
-
-// NOTE: This file is autogenerated by gopkgs.com.
-const (
-	goPkgsSrcPath = "github.com/rainycape/unidecode"
-	goPkgsName    = "unidecode"
-	goPkgsErrFmt  = "invalid import path %s - please use gopkgs.com/%s.v1 or see http://gopkgs.com/%s"
-)
-
-type goPkgsCheck struct{}
-
-func init() {
-	typ := reflect.TypeOf(goPkgsCheck{})
-	if typ.PkgPath() == goPkgsSrcPath {
-		panic(fmt.Errorf(goPkgsErrFmt, typ.PkgPath(), goPkgsName, goPkgsName))
-	}
-}

+ 4 - 2
Gruntfile.js

@@ -1,7 +1,6 @@
 /* jshint node:true */
 /* jshint node:true */
 'use strict';
 'use strict';
 module.exports = function (grunt) {
 module.exports = function (grunt) {
-
   var os = require('os');
   var os = require('os');
   var config = {
   var config = {
     pkg: grunt.file.readJSON('package.json'),
     pkg: grunt.file.readJSON('package.json'),
@@ -13,6 +12,10 @@ module.exports = function (grunt) {
     platform: process.platform.replace('win32', 'windows'),
     platform: process.platform.replace('win32', 'windows'),
   };
   };
 
 
+  if (process.platform.match(/^win/)) {
+    config.arch = process.env.hasOwnProperty('ProgramFiles(x86)') ? 'x64' : 'x86';
+  }
+
   config.pkg.version = grunt.option('pkgVer') || config.pkg.version;
   config.pkg.version = grunt.option('pkgVer') || config.pkg.version;
 
 
   // load plugins
   // load plugins
@@ -35,7 +38,6 @@ module.exports = function (grunt) {
 
 
   // Merge that object with what with whatever we have here
   // Merge that object with what with whatever we have here
   loadConfig(config,'./tasks/options/');
   loadConfig(config,'./tasks/options/');
-
   // pass the config to grunt
   // pass the config to grunt
   grunt.initConfig(config);
   grunt.initConfig(config);
 };
 };

+ 1 - 1
README.md

@@ -111,7 +111,7 @@ bra run
 
 
 ### Running
 ### Running
 ```
 ```
-./grafana web
+./grafana
 ```
 ```
 
 
 Open grafana in your browser (default http://localhost:3000) and login with admin user (default user/pass = admin/admin).
 Open grafana in your browser (default http://localhost:3000) and login with admin user (default user/pass = admin/admin).

+ 28 - 10
build.go

@@ -22,13 +22,16 @@ import (
 )
 )
 
 
 var (
 var (
-	versionRe        = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`)
-	goarch           string
-	goos             string
-	version          string = "v1"
-	race             bool
-	workingDir       string
-	serverBinaryName string = "grafana-server"
+	versionRe = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`)
+	goarch    string
+	goos      string
+	version   string = "v1"
+	// deb & rpm does not support semver so have to handle their version a little differently
+	linuxPackageVersion   string = "v1"
+	linuxPackageIteration string = ""
+	race                  bool
+	workingDir            string
+	serverBinaryName      string = "grafana-server"
 )
 )
 
 
 const minGoVersion = 1.3
 const minGoVersion = 1.3
@@ -40,7 +43,7 @@ func main() {
 	ensureGoPath()
 	ensureGoPath()
 	readVersionFromPackageJson()
 	readVersionFromPackageJson()
 
 
-	log.Printf("Version: %s\n", version)
+	log.Printf("Version: %s, Linux Version: %s, Package Iteration: %s\n", version, linuxPackageVersion, linuxPackageIteration)
 
 
 	flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
 	flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
 	flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
 	flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
@@ -70,7 +73,7 @@ func main() {
 
 
 		case "package":
 		case "package":
 			//verifyGitRepoIsClean()
 			//verifyGitRepoIsClean()
-			grunt("release", "--pkgVer="+version)
+			grunt("release")
 			createLinuxPackages()
 			createLinuxPackages()
 
 
 		case "latest":
 		case "latest":
@@ -107,6 +110,16 @@ func readVersionFromPackageJson() {
 	}
 	}
 
 
 	version = jsonObj["version"].(string)
 	version = jsonObj["version"].(string)
+	linuxPackageVersion = version
+	linuxPackageIteration = ""
+
+	// handle pre version stuff (deb / rpm does not support semver)
+	parts := strings.Split(version, "-")
+
+	if len(parts) > 1 {
+		linuxPackageVersion = parts[0]
+		linuxPackageIteration = parts[1]
+	}
 }
 }
 
 
 type linuxPackageOptions struct {
 type linuxPackageOptions struct {
@@ -208,10 +221,14 @@ func createPackage(options linuxPackageOptions) {
 		"--config-files", options.systemdServiceFilePath,
 		"--config-files", options.systemdServiceFilePath,
 		"--after-install", options.postinstSrc,
 		"--after-install", options.postinstSrc,
 		"--name", "grafana",
 		"--name", "grafana",
-		"--version", version,
+		"--version", linuxPackageVersion,
 		"-p", "./dist",
 		"-p", "./dist",
 	}
 	}
 
 
+	if linuxPackageIteration != "" {
+		args = append(args, "--iteration", linuxPackageIteration)
+	}
+
 	// add dependenciesj
 	// add dependenciesj
 	for _, dep := range options.depends {
 	for _, dep := range options.depends {
 		args = append(args, "--depends", dep)
 		args = append(args, "--depends", dep)
@@ -259,6 +276,7 @@ func grunt(params ...string) {
 
 
 func setup() {
 func setup() {
 	runPrint("go", "get", "-v", "github.com/tools/godep")
 	runPrint("go", "get", "-v", "github.com/tools/godep")
+	runPrint("go", "get", "-v", "github.com/blang/semver")
 	runPrint("go", "get", "-v", "github.com/mattn/go-sqlite3")
 	runPrint("go", "get", "-v", "github.com/mattn/go-sqlite3")
 	runPrint("go", "install", "-v", "github.com/mattn/go-sqlite3")
 	runPrint("go", "install", "-v", "github.com/mattn/go-sqlite3")
 }
 }

+ 4 - 2
conf/defaults.ini

@@ -7,7 +7,7 @@ app_mode = production
 
 
 #################################### Paths ####################################
 #################################### Paths ####################################
 [paths]
 [paths]
-# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is useD)
+# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
 #
 #
 data = data
 data = data
 #
 #
@@ -62,7 +62,7 @@ path = grafana.db
 
 
 #################################### Session ####################################
 #################################### Session ####################################
 [session]
 [session]
-# Either "memory", "file", "redis", "mysql", default is "memory"
+# Either "memory", "file", "redis", "mysql", "postgresql", default is "file"
 provider = file
 provider = file
 
 
 # Provider config options
 # Provider config options
@@ -70,6 +70,7 @@ provider = file
 # file: session dir path, is relative to grafana data_path
 # file: session dir path, is relative to grafana data_path
 # redis: config like redis server addr, poolSize, password, e.g. `127.0.0.1:6379,100,grafana`
 # redis: config like redis server addr, poolSize, password, e.g. `127.0.0.1:6379,100,grafana`
 # mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1)/database_name`
 # mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1)/database_name`
+
 provider_config = sessions
 provider_config = sessions
 
 
 # Session cookie name
 # Session cookie name
@@ -139,6 +140,7 @@ enabled = false
 client_id = some_id
 client_id = some_id
 client_secret = some_secret
 client_secret = some_secret
 scopes = user:email
 scopes = user:email
+team_ids =
 auth_url = https://github.com/login/oauth/authorize
 auth_url = https://github.com/login/oauth/authorize
 token_url = https://github.com/login/oauth/access_token
 token_url = https://github.com/login/oauth/access_token
 api_url = https://api.github.com/user
 api_url = https://api.github.com/user

+ 5 - 6
conf/sample.ini

@@ -7,7 +7,7 @@
 
 
 #################################### Paths ####################################
 #################################### Paths ####################################
 [paths]
 [paths]
-# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is useD)
+# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
 #
 #
 ;data = /var/lib/grafana
 ;data = /var/lib/grafana
 #
 #
@@ -62,7 +62,7 @@
 
 
 #################################### Session ####################################
 #################################### Session ####################################
 [session]
 [session]
-# Either "memory", "file", "redis", "mysql", default is "memory"
+# Either "memory", "file", "redis", "mysql", "postgresql", default is "file"
 ;provider = file
 ;provider = file
 
 
 # Provider config options
 # Provider config options
@@ -142,8 +142,8 @@
 ;auth_url = https://github.com/login/oauth/authorize
 ;auth_url = https://github.com/login/oauth/authorize
 ;token_url = https://github.com/login/oauth/access_token
 ;token_url = https://github.com/login/oauth/access_token
 ;api_url = https://api.github.com/user
 ;api_url = https://api.github.com/user
-# Uncomment bellow to only allow specific email domains
-; allowed_domains = mycompany.com othercompany.com
+;team_ids =
+;allowed_domains =
 
 
 #################################### Google Auth ##########################
 #################################### Google Auth ##########################
 [auth.google]
 [auth.google]
@@ -154,8 +154,7 @@
 ;auth_url = https://accounts.google.com/o/oauth2/auth
 ;auth_url = https://accounts.google.com/o/oauth2/auth
 ;token_url = https://accounts.google.com/o/oauth2/token
 ;token_url = https://accounts.google.com/o/oauth2/token
 ;api_url = https://www.googleapis.com/oauth2/v1/userinfo
 ;api_url = https://www.googleapis.com/oauth2/v1/userinfo
-# Uncomment bellow to only allow specific email domains
-; allowed_domains = mycompany.com othercompany.com
+;allowed_domains =
 
 
 #################################### Logging ##########################
 #################################### Logging ##########################
 [log]
 [log]

+ 1 - 1
docs/Makefile

@@ -44,7 +44,7 @@ docs-test: docs-build
 	$(DOCKER_RUN_DOCS) "$(DOCKER_DOCS_IMAGE)" ./test.sh
 	$(DOCKER_RUN_DOCS) "$(DOCKER_DOCS_IMAGE)" ./test.sh
 
 
 docs-build:
 docs-build:
-	git fetch https://github.com/grafana/grafana.git docs-1.x && git diff --name-status FETCH_HEAD...HEAD -- . > changed-files
+	git fetch https://github.com/grafana/grafana.git docs-2.0 && git diff --name-status FETCH_HEAD...HEAD -- . > changed-files
 	echo "$(GIT_BRANCH)" > GIT_BRANCH
 	echo "$(GIT_BRANCH)" > GIT_BRANCH
 	echo "$(GITCOMMIT)" > GITCOMMIT
 	echo "$(GITCOMMIT)" > GITCOMMIT
 	docker build -t "$(DOCKER_DOCS_IMAGE)" .
 	docker build -t "$(DOCKER_DOCS_IMAGE)" .

+ 4 - 4
docs/sources/index.md

@@ -4,9 +4,9 @@ page_keywords: grafana, introduction, documentation, about
 
 
 # About Grafana
 # About Grafana
 
 
-Grafana is a leading open source applications for visualizing large-scale measurement data. 
+Grafana is a leading open source applications for visualizing large-scale measurement data.
 
 
-It provides a powerful and elegant way to create, share, and explore data and dashboards from your disparate metric databases, either with your team or the world. 
+It provides a powerful and elegant way to create, share, and explore data and dashboards from your disparate metric databases, either with your team or the world.
 
 
 Grafana is most commonly used for Internet infrastructure and application analytics, but many use it in other domains including industrial sensors, home automation, weather, and process control.
 Grafana is most commonly used for Internet infrastructure and application analytics, but many use it in other domains including industrial sensors, home automation, weather, and process control.
 
 
@@ -16,7 +16,7 @@ Version 2.0 was released in April 2015: Grafana now ships with its own backend s
 
 
 ## Community Resources, Feedback, and Support
 ## Community Resources, Feedback, and Support
 
 
-Thousands of organizations large and small rely on Grafana, and we have a vibrant and active community that constantly inspires us. 
+Thousands of organizations large and small rely on Grafana, and we have a vibrant and active community that constantly inspires us.
 
 
 Please don't hesitate to [open a new issue on Github](https://github.com/grafana/grafana/issues) with your suggestions, ideas, and bug reports.
 Please don't hesitate to [open a new issue on Github](https://github.com/grafana/grafana/issues) with your suggestions, ideas, and bug reports.
 
 
@@ -35,4 +35,4 @@ If you have any trouble with Grafana, whether you can't get it set up or you jus
 
 
 ## License
 ## License
 
 
-By utilizing this software, you agree to the terms of the included license. Grafana is licensed under the Apache 2.0 agreement. See [LICENSE](https://github.com/grafana/grafana/blob/master/LICENSE.mdhttps://github.com/grafana/grafana/blob/master/LICENSE.md) for the full license terms. 
+By utilizing this software, you agree to the terms of the included license. Grafana is licensed under the Apache 2.0 agreement. See [LICENSE](https://github.com/grafana/grafana/blob/master/LICENSE.md) for the full license terms.

+ 18 - 4
docs/sources/installation/configuration.md

@@ -179,6 +179,7 @@ Client ID and a Client Secret. Specify these in the grafana config file. Example
     client_id = YOUR_GITHUB_APP_CLIENT_ID
     client_id = YOUR_GITHUB_APP_CLIENT_ID
     client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
     client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
     scopes = user:email
     scopes = user:email
+    team_ids =
     auth_url = https://github.com/login/oauth/authorize
     auth_url = https://github.com/login/oauth/authorize
     token_url = https://github.com/login/oauth/access_token
     token_url = https://github.com/login/oauth/access_token
     allow_sign_up = false
     allow_sign_up = false
@@ -189,6 +190,21 @@ now login or signup with your github accounts.
 You may allow users to sign-up via github auth by setting allow_sign_up to true. When this option is
 You may allow users to sign-up via github auth by setting allow_sign_up to true. When this option is
 set to true, any user successfully authenticating via github auth will be automatically signed up.
 set to true, any user successfully authenticating via github auth will be automatically signed up.
 
 
+### team_ids
+Require an active team membership for at least one of the given teams on GitHub.
+If the authenticated user isn't a member of at least one the teams they will not
+be able to register or authenticate with your Grafana instance. Example:
+
+    [auth.github]
+    enabled = true
+    client_id = YOUR_GITHUB_APP_CLIENT_ID
+    client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
+    scopes = user:email
+    team_ids = 150,300
+    auth_url = https://github.com/login/oauth/authorize
+    token_url = https://github.com/login/oauth/access_token
+    allow_sign_up = false
+
 ## [auth.google]
 ## [auth.google]
 You need to create a google project. You can do this in the [Google Developer Console](https://console.developers.google.com/project).
 You need to create a google project. You can do this in the [Google Developer Console](https://console.developers.google.com/project).
 When you create the project you will need to specify a callback URL. Specify this as callback:
 When you create the project you will need to specify a callback URL. Specify this as callback:
@@ -219,7 +235,7 @@ set to true, any user successfully authenticating via google auth will be automa
 ## [session]
 ## [session]
 
 
 ### provider
 ### provider
-Valid values are "memory", "file", "mysql", 'postgres'. Default is "memory".
+Valid values are "memory", "file", "mysql", 'postgres'. Default is "file".
 
 
 ### provider_config
 ### provider_config
 This option should be configured differently depending on what type of session provider you have configured.
 This option should be configured differently depending on what type of session provider you have configured.
@@ -252,10 +268,8 @@ How long sessions lasts in seconds. Defaults to `86400` (24 hours).
 When enabled Grafana will send anonymous usage statistics to stats.grafana.org.
 When enabled Grafana will send anonymous usage statistics to stats.grafana.org.
 No ip addresses are being tracked, only simple counters to track running instances,
 No ip addresses are being tracked, only simple counters to track running instances,
 versions, dashboard & error counts. It is very helpful to us, please leave this
 versions, dashboard & error counts. It is very helpful to us, please leave this
-enabled. Counters are sent every 24 hours.
+enabled. Counters are sent every 24 hours. Default value is `true`.
 
 
 ### google_analytics_ua_id
 ### google_analytics_ua_id
 If you want to track Grafana usage via Google analytics specify *your* Univeral Analytics ID
 If you want to track Grafana usage via Google analytics specify *your* Univeral Analytics ID
 here. By defualt this feature is disabled.
 here. By defualt this feature is disabled.
-
-

+ 2 - 2
docs/sources/installation/debian.md

@@ -10,11 +10,11 @@ page_keywords: grafana, installation, debian, ubuntu, guide
 
 
 Description | Download
 Description | Download
 ------------ | -------------
 ------------ | -------------
-.deb for Debian-based Linux | [grafana_2.0.1_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.0.1_amd64.deb)
+.deb for Debian-based Linux | [grafana_2.0.2_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.0.2_amd64.deb)
 
 
 ## Install
 ## Install
 
 
-    $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.0.1_amd64.deb
+    $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.0.2_amd64.deb
     $ sudo apt-get install -y adduser libfontconfig
     $ sudo apt-get install -y adduser libfontconfig
     $ sudo dpkg -i grafana_2.0.2_amd64.deb
     $ sudo dpkg -i grafana_2.0.2_amd64.deb
 
 

+ 4 - 4
docs/sources/installation/rpm.md

@@ -10,12 +10,12 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide
 
 
 Description | Download
 Description | Download
 ------------ | -------------
 ------------ | -------------
-.RPM for Fedora / RHEL / CentOS Linux | [grafana-2.0.1-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.0.1-1.x86_64.rpm)
+.RPM for Fedora / RHEL / CentOS Linux | [grafana-2.0.2-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.0.2-1.x86_64.rpm)
 
 
 ## Install
 ## Install
 You can install using yum
 You can install using yum
 
 
-    $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-2.0.1-1.x86_64.rpm
+    $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-2.0.2-1.x86_64.rpm
 
 
 Or manually using `rpm`
 Or manually using `rpm`
 
 
@@ -30,9 +30,9 @@ Add the following to a new file at `/etc/yum.repos.d/grafana.repo`
     name=grafana
     name=grafana
     baseurl=https://packagecloud.io/grafana/stable/el/6/$basearch
     baseurl=https://packagecloud.io/grafana/stable/el/6/$basearch
     repo_gpgcheck=1
     repo_gpgcheck=1
-    gpgcheck=0
     enabled=1
     enabled=1
-    gpgkey=https://packagecloud.io/gpg.key
+    gpgcheck=1
+    gpgkey=https://packagecloud.io/gpg.key https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana
     sslverify=1
     sslverify=1
     sslcacert=/etc/pki/tls/certs/ca-bundle.crt
     sslcacert=/etc/pki/tls/certs/ca-bundle.crt
 
 

+ 17 - 2
docs/sources/installation/windows.md

@@ -6,8 +6,23 @@ page_keywords: grafana, installation, windows guide
 
 
 # Installing on Windows
 # Installing on Windows
 
 
-There are currently no binary build for Windows. But read the [build from source](../project/building_from_source)
-page for instructions on how to build it yourself.
+## Download
+
+Description | Download
+------------ | -------------
+Zip package for Windows | [grafana.2.0.2.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-2.0.2.windows-x64.zip)
+
+## Configure
+The zip file contains a folder with the current grafana version. Extract this folder to anywhere you want Grafana to run from.
+Go into the `conf` directory and copy `sample.ini` to `custom.ini`. You should edit `custom.ini`, never `defaults.ini`.
+
+The default grafana port is `3000`, this port requires extra permissions on windows. Edit `custom.ini` and uncomment the `http_port`
+config and change it to something like `8080` or similar. That port should not require extra windows privileges.
+
+Start grafana by executing `grafana-server.exe`, preferbly from the command line. If you want to run Grafana as
+windows service, download [NSSM](https://nssm.cc/). It is very easy add grafana as windows service using that tool.
+
+Read more about the [configuration options](configuration.md).
 
 
 ## Building on Windows
 ## Building on Windows
 
 

+ 143 - 1
docs/sources/reference/http_api.md

@@ -6,5 +6,147 @@ page_keywords: grafana, admin, http, api, documentation
 
 
 # HTTP API Reference
 # HTTP API Reference
 
 
-This documentation page has yet to be written.
+The Grafana backend exposes an HTTP API, the same API is used by the frontend to do everything from saving
+dashboards, creating users and updating data sources.
+
+## Authorization
+
+Currently you can authenticate via an `API Token` or via a `Session cookie` (acquired using regular login or oauth).
+
+### Create API Token
+
+Open the sidemenu and click the organization dropdown and select the `API Keys` option.
+
+![](/img/v2/orgdropdown_api_keys.png)
+
+You use the token in all requests in the `Authorization` header, like this:
+
+**Example**:
+
+        GET http://your.grafana.com/api/dashboards/db/mydash HTTP/1.1
+        Accept: application/json
+        Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+The `Authorization` header value should be `Bearer <your api key>`.
+
+## Dashboards
+
+### Create / Update dashboard
+
+`POST /api/dashboards/db`
+
+Creates a new dashboard or updates an existing dashboard.
+
+**Example Request for new dashboard**:
+
+        POST /api/dashboards/db HTTP/1.1
+        Accept: application/json
+        Content-Type: application/json
+        Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+        {
+          "dashboard": {
+            "id": null,
+            "title": "Production Overview",
+            "tags": [ "templated" ],
+            "timezone": "browser",
+            "rows": [
+              {
+              }
+            ]
+            "schemaVersion": 6,
+            "version": 0
+          },
+          "overwrite": false
+        }
+
+JSON Body schema:
+
+- **dashboard** – The complete dashboard model, id = null to create a new dashboard
+- **overwrite** – Set to true if you want to overwrite existing dashboard with newer version or with same dashboard title.
+
+**Example Response**:
+
+    HTTP/1.1 200 OK
+    Content-Type: application/json; charset=UTF-8
+    Content-Length: 78
+
+    {
+      "slug": "production-overview",
+      "status": "success",
+      "version": 1
+    }
+
+Status Codes:
+
+- **200** – Created
+- **400** – Errors (invalid json, missing or invalid fields, etc)
+- **401** – Unauthorized
+- **412** – Precondition failed
+
+The **412** status code is used when a newer dashboard already exists (newer, its version is greater than the verison that was sent). The
+same status code is also used if another dashboar exists with the same title. The response body will look like this:
+
+    HTTP/1.1 412 Precondition Failed
+    Content-Type: application/json; charset=UTF-8
+    Content-Length: 97
+
+    {
+      "message": "The dashboard has been changed by someone else",
+      "status": "version-mismatch"
+    }
+
+In in case of title already exists the `status` property will be `name-exists`.
+
+### Get dashboard
+
+`GET /api/dashboards/db/:slug`
+
+Will return the dashboard given the dashboard slug. Slug is the url friendly version of the dashboard title.
+
+**Example Request**:
+
+        GET /api/dashboards/db/production-overview HTTP/1.1
+        Accept: application/json
+        Content-Type: application/json
+        Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+        HTTP/1.1 200
+        Content-Type: application/json
+
+        {
+          "meta": {
+            "isStarred": false,
+            "slug": "production-overview"
+          },
+          "dashboard": {
+            "id": null,
+            "title": "Production Overview",
+            "tags": [ "templated" ],
+            "timezone": "browser",
+            "rows": [
+              {
+              }
+            ]
+            "schemaVersion": 6,
+            "version": 0
+          },
+        }
+
+### Delete dashboard
+
+`DELETE /api/dashboards/db/:slug`
+
+The above will delete the dashboard with the specified slug. The slug is the url friendly (unique) version of the dashboard title.
+
+## Data sources
+
+### Create data source
+
+## Organizations
+
+## Users
+
 
 

+ 1 - 1
docs/sources/reference/timerange.md

@@ -24,7 +24,7 @@ All of this applies to all Panels in the Dashboard (except those with Panel Time
 
 
 It's possible to customize the options displayed for relative time and the auto-refresh options. 
 It's possible to customize the options displayed for relative time and the auto-refresh options. 
 
 
-From Dashboard setttings, click the Timepicker tab. From here you can specify the relative and auto refresh intervals. The Timepicker tab settings are saved on a per Dashboard basis.
+From Dashboard setttings, click the Timepicker tab. From here you can specify the relative and auto refresh intervals. The Timepicker tab settings are saved on a per Dashboard basis.  Entries are comma seperated and accept a number followed by one of the following units: s (seconds), m (minutes), h (hours), d (days), w (weeks), M (months), y (years).
 
 
 ![](/img/v1/timepicker_editor.png)
 ![](/img/v1/timepicker_editor.png)
 
 

+ 1 - 1
latest.json

@@ -1,3 +1,3 @@
 {
 {
-	"version": "2.0.0-beta3",
+	"version": "2.0.1",
 }
 }

+ 1 - 1
package.json

@@ -4,7 +4,7 @@
     "company": "Coding Instinct AB"
     "company": "Coding Instinct AB"
   },
   },
   "name": "grafana",
   "name": "grafana",
-  "version": "2.0.1",
+  "version": "2.1.0-pre1",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
     "url": "http://github.com/torkelo/grafana.git"
     "url": "http://github.com/torkelo/grafana.git"

+ 5 - 1
packaging/deb/init.d/grafana-server

@@ -87,6 +87,7 @@ case "$1" in
     # check if pid file has been written two
     # check if pid file has been written two
 	  if ! [[ -s $PID_FILE ]]; then
 	  if ! [[ -s $PID_FILE ]]; then
 	    log_end_msg 1
 	    log_end_msg 1
+	    exit 1
 	  fi
 	  fi
 
 
 		i=0
 		i=0
@@ -96,7 +97,10 @@ case "$1" in
 		do
 		do
 			sleep 1
 			sleep 1
 			i=$(($i + 1))
 			i=$(($i + 1))
-			[ $i -gt $timeout ] && log_end_msg 1
+      if [ $i -gt $timeout ]; then
+			  log_end_msg 1
+			  exit 1
+			fi
 		done
 		done
   fi
   fi
   log_end_msg $return
   log_end_msg $return

+ 0 - 2
packaging/rpm/init.d/grafana-server

@@ -144,5 +144,3 @@ case "$1" in
     exit 1
     exit 1
     ;;
     ;;
 esac
 esac
-
-exit 0

+ 5 - 4
pkg/api/dtos/models.go

@@ -21,16 +21,17 @@ type CurrentUser struct {
 	Email          string     `json:"email"`
 	Email          string     `json:"email"`
 	Name           string     `json:"name"`
 	Name           string     `json:"name"`
 	LightTheme     bool       `json:"lightTheme"`
 	LightTheme     bool       `json:"lightTheme"`
-	OrgRole        m.RoleType `json:"orgRole"`
+	OrgId          int64      `json:"orgId"`
 	OrgName        string     `json:"orgName"`
 	OrgName        string     `json:"orgName"`
+	OrgRole        m.RoleType `json:"orgRole"`
 	IsGrafanaAdmin bool       `json:"isGrafanaAdmin"`
 	IsGrafanaAdmin bool       `json:"isGrafanaAdmin"`
 	GravatarUrl    string     `json:"gravatarUrl"`
 	GravatarUrl    string     `json:"gravatarUrl"`
 }
 }
 
 
 type DashboardMeta struct {
 type DashboardMeta struct {
-	IsStarred  bool      `json:"isStarred"`
-	IsHome     bool      `json:"isHome"`
-	IsSnapshot bool      `json:"isSnapshot"`
+	IsStarred  bool      `json:"isStarred,omitempty"`
+	IsHome     bool      `json:"isHome,omitempty"`
+	IsSnapshot bool      `json:"isSnapshot,omitempty"`
 	Slug       string    `json:"slug"`
 	Slug       string    `json:"slug"`
 	Expires    time.Time `json:"expires"`
 	Expires    time.Time `json:"expires"`
 	Created    time.Time `json:"created"`
 	Created    time.Time `json:"created"`

+ 1 - 0
pkg/api/index.go

@@ -18,6 +18,7 @@ func setIndexViewData(c *middleware.Context) error {
 		Email:          c.Email,
 		Email:          c.Email,
 		Name:           c.Name,
 		Name:           c.Name,
 		LightTheme:     c.Theme == "light",
 		LightTheme:     c.Theme == "light",
+		OrgId:          c.OrgId,
 		OrgName:        c.OrgName,
 		OrgName:        c.OrgName,
 		OrgRole:        c.OrgRole,
 		OrgRole:        c.OrgRole,
 		GravatarUrl:    dtos.GetGravatarUrl(c.Email),
 		GravatarUrl:    dtos.GetGravatarUrl(c.Email),

+ 7 - 2
pkg/api/login_oauth.go

@@ -3,6 +3,7 @@ package api
 import (
 import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
+	"net/url"
 
 
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2"
 
 
@@ -45,7 +46,11 @@ func OAuthLogin(ctx *middleware.Context) {
 
 
 	userInfo, err := connect.UserInfo(token)
 	userInfo, err := connect.UserInfo(token)
 	if err != nil {
 	if err != nil {
-		ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
+		if err == social.ErrMissingTeamMembership {
+			ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github team membership not fulfilled"))
+		} else {
+			ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
+		}
 		return
 		return
 	}
 	}
 
 
@@ -54,7 +59,7 @@ func OAuthLogin(ctx *middleware.Context) {
 	// validate that the email is allowed to login to grafana
 	// validate that the email is allowed to login to grafana
 	if !connect.IsEmailAllowed(userInfo.Email) {
 	if !connect.IsEmailAllowed(userInfo.Email) {
 		log.Info("OAuth login attempt with unallowed email, %s", userInfo.Email)
 		log.Info("OAuth login attempt with unallowed email, %s", userInfo.Email)
-		ctx.Redirect(setting.AppSubUrl + "/login?email_not_allowed=1")
+		ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required email domain not fulfilled"))
 		return
 		return
 	}
 	}
 
 

+ 1 - 1
pkg/models/dashboards.go

@@ -5,7 +5,7 @@ import (
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
-	"github.com/gosimple/slug"
+	"github.com/dalu/slug"
 )
 )
 
 
 // Typed errors
 // Typed errors

+ 6 - 0
pkg/services/sqlstore/migrations/dashboard_mig.go

@@ -86,4 +86,10 @@ func addDashboardMigration(mg *Migrator) {
 	}))
 	}))
 
 
 	mg.AddMigration("drop table dashboard_v1", NewDropTableMigration("dashboard_v1"))
 	mg.AddMigration("drop table dashboard_v1", NewDropTableMigration("dashboard_v1"))
+
+	// change column type of dashboard.data
+	mg.AddMigration("alter dashboard.data to mediumtext v1", new(RawSqlMigration).
+		Sqlite("SELECT 0 WHERE 0;").
+		Postgres("SELECT 0;").
+		Mysql("ALTER TABLE dashboard MODIFY data MEDIUMTEXT;"))
 }
 }

+ 6 - 0
pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go

@@ -48,4 +48,10 @@ func addDashboardSnapshotMigrations(mg *Migrator) {
 
 
 	mg.AddMigration("create dashboard_snapshot table v5 #2", NewAddTableMigration(snapshotV5))
 	mg.AddMigration("create dashboard_snapshot table v5 #2", NewAddTableMigration(snapshotV5))
 	addTableIndicesMigrations(mg, "v5", snapshotV5)
 	addTableIndicesMigrations(mg, "v5", snapshotV5)
+
+	// change column type of dashboard
+	mg.AddMigration("alter dashboard_snapshot to mediumtext v2", new(RawSqlMigration).
+		Sqlite("SELECT 0 WHERE 0;").
+		Postgres("SELECT 0;").
+		Mysql("ALTER TABLE dashboard_snapshot MODIFY dashboard MEDIUMTEXT;"))
 }
 }

+ 10 - 2
pkg/services/sqlstore/migrator/migrations.go

@@ -25,8 +25,9 @@ func (m *MigrationBase) GetCondition() MigrationCondition {
 type RawSqlMigration struct {
 type RawSqlMigration struct {
 	MigrationBase
 	MigrationBase
 
 
-	sqlite string
-	mysql  string
+	sqlite   string
+	mysql    string
+	postgres string
 }
 }
 
 
 func (m *RawSqlMigration) Sql(dialect Dialect) string {
 func (m *RawSqlMigration) Sql(dialect Dialect) string {
@@ -35,6 +36,8 @@ func (m *RawSqlMigration) Sql(dialect Dialect) string {
 		return m.mysql
 		return m.mysql
 	case SQLITE:
 	case SQLITE:
 		return m.sqlite
 		return m.sqlite
+	case POSTGRES:
+		return m.postgres
 	}
 	}
 
 
 	panic("db type not supported")
 	panic("db type not supported")
@@ -50,6 +53,11 @@ func (m *RawSqlMigration) Mysql(sql string) *RawSqlMigration {
 	return m
 	return m
 }
 }
 
 
+func (m *RawSqlMigration) Postgres(sql string) *RawSqlMigration {
+	m.postgres = sql
+	return m
+}
+
 type AddColumnMigration struct {
 type AddColumnMigration struct {
 	MigrationBase
 	MigrationBase
 	tableName string
 	tableName string

+ 60 - 8
pkg/social/social.go

@@ -2,7 +2,9 @@ package social
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"fmt"
+	"net/http"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 
 
@@ -75,13 +77,24 @@ func NewOAuthService() {
 		// GitHub.
 		// GitHub.
 		if name == "github" {
 		if name == "github" {
 			setting.OAuthService.GitHub = true
 			setting.OAuthService.GitHub = true
-			SocialMap["github"] = &SocialGithub{Config: &config, allowedDomains: info.AllowedDomains, ApiUrl: info.ApiUrl, allowSignup: info.AllowSignup}
+			teamIds := sec.Key("team_ids").Ints(",")
+			SocialMap["github"] = &SocialGithub{
+				Config:         &config,
+				allowedDomains: info.AllowedDomains,
+				apiUrl:         info.ApiUrl,
+				allowSignup:    info.AllowSignup,
+				teamIds:        teamIds,
+			}
 		}
 		}
 
 
 		// Google.
 		// Google.
 		if name == "google" {
 		if name == "google" {
 			setting.OAuthService.Google = true
 			setting.OAuthService.Google = true
-			SocialMap["google"] = &SocialGoogle{Config: &config, allowedDomains: info.AllowedDomains, ApiUrl: info.ApiUrl, allowSignup: info.AllowSignup}
+			SocialMap["google"] = &SocialGoogle{
+				Config: &config, allowedDomains: info.AllowedDomains,
+				apiUrl:      info.ApiUrl,
+				allowSignup: info.AllowSignup,
+			}
 		}
 		}
 	}
 	}
 }
 }
@@ -103,10 +116,15 @@ func isEmailAllowed(email string, allowedDomains []string) bool {
 type SocialGithub struct {
 type SocialGithub struct {
 	*oauth2.Config
 	*oauth2.Config
 	allowedDomains []string
 	allowedDomains []string
-	ApiUrl         string
+	apiUrl         string
 	allowSignup    bool
 	allowSignup    bool
+	teamIds        []int
 }
 }
 
 
+var (
+	ErrMissingTeamMembership = errors.New("User not a member of one of the required teams")
+)
+
 func (s *SocialGithub) Type() int {
 func (s *SocialGithub) Type() int {
 	return int(models.GITHUB)
 	return int(models.GITHUB)
 }
 }
@@ -119,6 +137,28 @@ func (s *SocialGithub) IsSignupAllowed() bool {
 	return s.allowSignup
 	return s.allowSignup
 }
 }
 
 
+func (s *SocialGithub) IsTeamMember(client *http.Client, username string, teamId int) bool {
+	var data struct {
+		Url   string `json:"url"`
+		State string `json:"state"`
+	}
+
+	membershipUrl := fmt.Sprintf("https://api.github.com/teams/%d/memberships/%s", teamId, username)
+	r, err := client.Get(membershipUrl)
+	if err != nil {
+		return false
+	}
+
+	defer r.Body.Close()
+
+	if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
+		return false
+	}
+
+	active := data.State == "active"
+	return active
+}
+
 func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
 func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
 	var data struct {
 	var data struct {
 		Id    int    `json:"id"`
 		Id    int    `json:"id"`
@@ -128,7 +168,7 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
 
 
 	var err error
 	var err error
 	client := s.Client(oauth2.NoContext, token)
 	client := s.Client(oauth2.NoContext, token)
-	r, err := client.Get(s.ApiUrl)
+	r, err := client.Get(s.apiUrl)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -139,11 +179,23 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	return &BasicUserInfo{
+	userInfo := &BasicUserInfo{
 		Identity: strconv.Itoa(data.Id),
 		Identity: strconv.Itoa(data.Id),
 		Name:     data.Name,
 		Name:     data.Name,
 		Email:    data.Email,
 		Email:    data.Email,
-	}, nil
+	}
+
+	if len(s.teamIds) > 0 {
+		for _, teamId := range s.teamIds {
+			if s.IsTeamMember(client, data.Name, teamId) {
+				return userInfo, nil
+			}
+		}
+
+		return nil, ErrMissingTeamMembership
+	} else {
+		return userInfo, nil
+	}
 }
 }
 
 
 //   ________                     .__
 //   ________                     .__
@@ -156,7 +208,7 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
 type SocialGoogle struct {
 type SocialGoogle struct {
 	*oauth2.Config
 	*oauth2.Config
 	allowedDomains []string
 	allowedDomains []string
-	ApiUrl         string
+	apiUrl         string
 	allowSignup    bool
 	allowSignup    bool
 }
 }
 
 
@@ -181,7 +233,7 @@ func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
 	var err error
 	var err error
 
 
 	client := s.Client(oauth2.NoContext, token)
 	client := s.Client(oauth2.NoContext, token)
-	r, err := client.Get(s.ApiUrl)
+	r, err := client.Get(s.apiUrl)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}

+ 6 - 0
public/app/components/kbn.js

@@ -380,6 +380,9 @@ 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.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.joule = kbn.formatFuncCreator(1000, [' J', ' kJ', ' MJ', ' GJ', ' TJ', ' PJ', ' EJ', ' ZJ', ' YJ']);
+  kbn.valueFormats.amp = kbn.formatFuncCreator(1000, [' A', ' kA', ' MA', ' GA', ' TA', ' PA', ' EA', ' ZA', ' YA']);
+  kbn.valueFormats.volt = kbn.formatFuncCreator(1000, [' V', ' kV', ' MV', ' GV', ' TV', ' PV', ' EV', ' ZV', ' YV']);
+  kbn.valueFormats.hertz = kbn.formatFuncCreator(1000, [' Hz', ' kHz', ' MHz', ' GHz', ' THz', ' PHz', ' EHz', ' ZHz', ' YHz']);
   kbn.valueFormats.watt = kbn.formatFuncCreator(1000, [' W', ' kW', ' MW', ' GW', ' TW', ' PW', ' EW', ' ZW', ' YW']);
   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.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.watth = kbn.formatFuncCreator(1000, [' Wh', ' kWh', ' MWh', ' GWh', ' TWh', ' PWh', ' EWh', ' ZWh', ' YWh']);
@@ -534,6 +537,7 @@ function($, _, moment) {
           {text: 'microseconds (µs)', value: 'µs'},
           {text: 'microseconds (µs)', value: 'µs'},
           {text: 'milliseconds (ms)', value: 'ms'},
           {text: 'milliseconds (ms)', value: 'ms'},
           {text: 'seconds (s)', value: 's'},
           {text: 'seconds (s)', value: 's'},
+          {text: 'Hertz (1/s)', value: 'hertz'},
         ]
         ]
       },
       },
       {
       {
@@ -561,6 +565,8 @@ function($, _, moment) {
           {text: 'kilowatt-hour (kWh)',   value: 'kwatth'},
           {text: 'kilowatt-hour (kWh)',   value: 'kwatth'},
           {text: 'joule (J)',             value: 'joule'},
           {text: 'joule (J)',             value: 'joule'},
           {text: 'electron volt (eV)',    value: 'ev'},
           {text: 'electron volt (eV)',    value: 'ev'},
+          {text: 'Ampere (A)',            value: 'amp'},
+          {text: 'Volt (V)',              value: 'volt'},
         ]
         ]
       },
       },
       {
       {

+ 8 - 1
public/app/controllers/loginCtrl.js

@@ -7,7 +7,7 @@ function (angular, config) {
 
 
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');
 
 
-  module.controller('LoginCtrl', function($scope, backendSrv, contextSrv) {
+  module.controller('LoginCtrl', function($scope, backendSrv, contextSrv, $location) {
     $scope.formModel = {
     $scope.formModel = {
       user: '',
       user: '',
       email: '',
       email: '',
@@ -28,6 +28,13 @@ function (angular, config) {
     $scope.init = function() {
     $scope.init = function() {
       $scope.$watch("loginMode", $scope.loginModeChanged);
       $scope.$watch("loginMode", $scope.loginModeChanged);
       $scope.passwordChanged();
       $scope.passwordChanged();
+
+      var params = $location.search();
+      if (params.failedMsg) {
+        $scope.appEvent('alert-warning', ['Login Failed', params.failedMsg]);
+        delete params.failedMsg;
+        $location.search(params);
+      }
     };
     };
 
 
     // build info view model
     // build info view model

+ 6 - 0
public/app/features/dashboard/dashboardNavCtrl.js

@@ -52,6 +52,10 @@ function (angular, _) {
     };
     };
 
 
     $scope.saveDashboard = function(options) {
     $scope.saveDashboard = function(options) {
+      if ($scope.dashboardMeta.canSave === false) {
+        return;
+      }
+
       var clone = $scope.dashboard.getSaveModelClone();
       var clone = $scope.dashboard.getSaveModelClone();
 
 
       backendSrv.saveDashboard(clone, options).then(function(data) {
       backendSrv.saveDashboard(clone, options).then(function(data) {
@@ -119,6 +123,8 @@ function (angular, _) {
     $scope.saveDashboardAs = function() {
     $scope.saveDashboardAs = function() {
       var newScope = $rootScope.$new();
       var newScope = $rootScope.$new();
       newScope.clone = $scope.dashboard.getSaveModelClone();
       newScope.clone = $scope.dashboard.getSaveModelClone();
+      newScope.clone.editable = true;
+      newScope.clone.hideControls = false;
 
 
       $scope.appEvent('show-modal', {
       $scope.appEvent('show-modal', {
         src: './app/features/dashboard/partials/saveDashboardAs.html',
         src: './app/features/dashboard/partials/saveDashboardAs.html',

+ 10 - 12
public/app/features/dashboard/dashboardSrv.js

@@ -52,24 +52,22 @@ function (angular, $, kbn, _, moment) {
 
 
     p._initMeta = function(meta) {
     p._initMeta = function(meta) {
       meta = meta || {};
       meta = meta || {};
-      meta.canShare = true;
-      meta.canSave = true;
-      meta.canEdit = true;
-      meta.canStar = true;
 
 
-      if (contextSrv.hasRole('Viewer')) {
-        meta.canSave = false;
-      }
+      meta.canShare = meta.canShare === false ? false : true;
+      meta.canSave = meta.canSave === false ? false : true;
+      meta.canEdit = meta.canEdit === false ? false : true;
+      meta.canStar = meta.canStar === false ? false : true;
+      meta.canDelete = meta.canDelete === false ? false : true;
 
 
-      if (meta.isSnapshot) {
+      if (contextSrv.hasRole('Viewer')) {
         meta.canSave = false;
         meta.canSave = false;
       }
       }
 
 
-      if (meta.isHome) {
-        meta.canShare = false;
-        meta.canStar = false;
-        meta.canSave = false;
+      if (!this.editable) {
         meta.canEdit = false;
         meta.canEdit = false;
+        meta.canDelete = false;
+        meta.canSave = false;
+        this.hideControls = true;
       }
       }
 
 
       this.meta = meta;
       this.meta = meta;

+ 6 - 6
public/app/features/dashboard/partials/dashboardTopNav.html

@@ -30,16 +30,16 @@
 				<li ng-show="dashboardMeta.canSave">
 				<li ng-show="dashboardMeta.canSave">
 					<a ng-click="saveDashboard()" bs-tooltip="'Save dashboard'" data-placement="bottom"><i class="fa fa-save"></i></a>
 					<a ng-click="saveDashboard()" bs-tooltip="'Save dashboard'" data-placement="bottom"><i class="fa fa-save"></i></a>
 				</li>
 				</li>
-				<li class="dropdown" ng-if="dashboardMeta.canEdit">
+				<li class="dropdown">
 					<a class="pointer" data-toggle="dropdown"><i class="fa fa-cog"></i></a>
 					<a class="pointer" data-toggle="dropdown"><i class="fa fa-cog"></i></a>
 					<ul class="dropdown-menu">
 					<ul class="dropdown-menu">
-						<li><a class="pointer" ng-click="openEditView('settings');">Settings</a></li>
-						<li><a class="pointer" ng-click="openEditView('annotations');">Annotations</a></li>
-						<li><a class="pointer" ng-click="openEditView('templating');">Templating</a></li>
+						<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('settings');">Settings</a></li>
+						<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('annotations');">Annotations</a></li>
+						<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('templating');">Templating</a></li>
 						<li><a class="pointer" ng-click="exportDashboard();">Export</a></li>
 						<li><a class="pointer" ng-click="exportDashboard();">Export</a></li>
 						<li><a class="pointer" ng-click="editJson();">View JSON</a></li>
 						<li><a class="pointer" ng-click="editJson();">View JSON</a></li>
-						<li ng-if="dashboardMeta.canSave"><a class="pointer" ng-click="saveDashboardAs();">Save As...</a></li>
-						<li ng-if="dashboardMeta.canSave"><a class="pointer" ng-click="deleteDashboard();">Delete dashboard</a></li>
+						<li ng-if="contextSrv.isEditor"><a class="pointer" ng-click="saveDashboardAs();">Save As...</a></li>
+						<li ng-if="dashboardMeta.canDelete"><a class="pointer" ng-click="deleteDashboard();">Delete dashboard</a></li>
 					</ul>
 					</ul>
 				</li>
 				</li>
 			</ul>
 			</ul>

+ 1 - 1
public/app/features/dashboard/unsavedChangesSrv.js

@@ -37,7 +37,7 @@ function(angular, _, config) {
     });
     });
 
 
     this.ignoreChanges = function() {
     this.ignoreChanges = function() {
-      if (!self.current) { return true; }
+      if (!self.current || !self.current.meta) { return true; }
 
 
       var meta = self.current.meta;
       var meta = self.current.meta;
       return !meta.canSave || meta.fromScript || meta.fromFile;
       return !meta.canSave || meta.fromScript || meta.fromFile;

+ 1 - 1
public/app/features/panel/panelHelper.js

@@ -43,7 +43,7 @@ function (angular, _, kbn, $) {
       }
       }
 
 
       if (scope.panel.timeShift) {
       if (scope.panel.timeShift) {
-        if (!kbn.isValidTimeSpan(scope.panel.timeFrom)) {
+        if (!kbn.isValidTimeSpan(scope.panel.timeShift)) {
           scope.panelMeta.timeInfo = 'invalid timeshift';
           scope.panelMeta.timeInfo = 'invalid timeshift';
           return;
           return;
         }
         }

+ 8 - 0
public/app/features/panel/panelSrv.js

@@ -70,6 +70,14 @@ function (angular, _, config) {
       };
       };
 
 
       $scope.toggleFullscreen = function(edit) {
       $scope.toggleFullscreen = function(edit) {
+        if (edit && $scope.dashboardMeta.canEdit === false) {
+          $scope.appEvent('alert-warning', [
+            'Dashboard not editable',
+            'Use Save As.. feature to create an editable copy of this dashboard.'
+          ]);
+          return;
+        }
+
         $scope.dashboardViewState.update({ fullscreen: true, edit: edit, panelId: $scope.panel.id });
         $scope.dashboardViewState.update({ fullscreen: true, edit: edit, panelId: $scope.panel.id });
       };
       };
 
 

+ 21 - 8
public/app/features/templating/templateValuesSrv.js

@@ -29,13 +29,7 @@ function (angular, _, kbn) {
         var variable = this.variables[i];
         var variable = this.variables[i];
         var urlValue = queryParams['var-' + variable.name];
         var urlValue = queryParams['var-' + variable.name];
         if (urlValue !== void 0) {
         if (urlValue !== void 0) {
-          var option = _.findWhere(variable.options, { text: urlValue });
-          option = option || { text: urlValue, value: urlValue };
-
-          var promise = this.setVariableValue(variable, option, true);
-          this.updateAutoInterval(variable);
-
-          promises.push(promise);
+          promises.push(this.setVariableFromUrl(variable, urlValue));
         }
         }
         else if (variable.refresh) {
         else if (variable.refresh) {
           promises.push(this.updateOptions(variable));
           promises.push(this.updateOptions(variable));
@@ -48,11 +42,30 @@ function (angular, _, kbn) {
       return $q.all(promises);
       return $q.all(promises);
     };
     };
 
 
+    this.setVariableFromUrl = function(variable, urlValue) {
+      if (variable.refresh) {
+        var self = this;
+        //refresh the list of options before setting the value
+        return this.updateOptions(variable).then(function() {
+          var option = _.findWhere(variable.options, { text: urlValue });
+          option = option || { text: urlValue, value: urlValue };
+
+          self.updateAutoInterval(variable);
+          return self.setVariableValue(variable, option);
+        });
+      }
+      var option = _.findWhere(variable.options, { text: urlValue });
+      option = option || { text: urlValue, value: urlValue };
+
+      this.updateAutoInterval(variable);
+      return this.setVariableValue(variable, option);
+    };
+
     this.updateAutoInterval = function(variable) {
     this.updateAutoInterval = function(variable) {
       if (!variable.auto) { return; }
       if (!variable.auto) { return; }
 
 
       // add auto option if missing
       // add auto option if missing
-      if (variable.options[0].text !== 'auto') {
+      if (variable.options.length && variable.options[0].text !== 'auto') {
         variable.options.unshift({ text: 'auto', value: '$__auto_interval' });
         variable.options.unshift({ text: 'auto', value: '$__auto_interval' });
       }
       }
 
 

+ 2 - 3
public/app/panels/graph/axisEditor.html

@@ -228,10 +228,10 @@
 		</div>
 		</div>
 		<div class="tight-form last">
 		<div class="tight-form last">
 			<ul class="tight-form-list">
 			<ul class="tight-form-list">
-				<li class="tight-form-item" style="width: 100px">
+				<li class="tight-form-item" style="width: 105px">
 					<strong>Decimals</strong>
 					<strong>Decimals</strong>
 				</li>
 				</li>
-				<li>
+				<li style="width: 105px">
 					<input type="number" class="input-small tight-form-input" placeholder="auto" bs-tooltip="'Override automatic decimal precision for legend and tooltips'" data-placement="right"
 					<input type="number" class="input-small tight-form-input" placeholder="auto" bs-tooltip="'Override automatic decimal precision for legend and tooltips'" data-placement="right"
 					ng-model="panel.decimals" ng-change="render()" ng-model-onblur>
 					ng-model="panel.decimals" ng-change="render()" ng-model-onblur>
 				</li>
 				</li>
@@ -242,4 +242,3 @@
 
 
 	</div>
 	</div>
 </div>
 </div>
-

+ 4 - 3
public/app/panels/graph/graph.js

@@ -63,12 +63,13 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
           render_panel();
           render_panel();
         });
         });
 
 
-        function getLegendHeight() {
+        function getLegendHeight(panelHeight) {
           if (!scope.panel.legend.show || scope.panel.legend.rightSide) {
           if (!scope.panel.legend.show || scope.panel.legend.rightSide) {
             return 0;
             return 0;
           }
           }
           if (scope.panel.legend.alignAsTable) {
           if (scope.panel.legend.alignAsTable) {
-            return 30 + (25 * data.length);
+            var total = 30 + (25 * data.length);
+            return Math.min(total, Math.floor(panelHeight/2));
           } else {
           } else {
             return 26;
             return 26;
           }
           }
@@ -84,7 +85,7 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
             graphHeight -= 5; // padding
             graphHeight -= 5; // padding
             graphHeight -= scope.panel.title ? 24 : 9; // subtract panel title bar
             graphHeight -= scope.panel.title ? 24 : 9; // subtract panel title bar
 
 
-            graphHeight = graphHeight - getLegendHeight(); // subtract one line legend
+            graphHeight = graphHeight - getLegendHeight(graphHeight); // subtract one line legend
 
 
             elem.css('height', graphHeight + 'px');
             elem.css('height', graphHeight + 'px');
 
 

+ 3 - 3
public/app/panels/graph/module.js

@@ -29,7 +29,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
       panelName: 'Graph',
       panelName: 'Graph',
       editIcon:  "fa fa-bar-chart",
       editIcon:  "fa fa-bar-chart",
       fullscreen: true,
       fullscreen: true,
-      metricsEditor: true
+      metricsEditor: true,
     });
     });
 
 
     $scope.panelMeta.addEditorTab('Axes & Grid', 'app/panels/graph/axisEditor.html');
     $scope.panelMeta.addEditorTab('Axes & Grid', 'app/panels/graph/axisEditor.html');
@@ -67,9 +67,9 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
       // show/hide lines
       // show/hide lines
       lines         : true,
       lines         : true,
       // fill factor
       // fill factor
-      fill          : 0,
+      fill          : 1,
       // line width in pixels
       // line width in pixels
-      linewidth     : 1,
+      linewidth     : 2,
       // show hide points
       // show hide points
       points        : false,
       points        : false,
       // point radius in pixels
       // point radius in pixels

+ 2 - 2
public/app/partials/dashboard.html

@@ -24,7 +24,7 @@
 						<div class="row-text pointer" ng-click="toggle_row(row)" ng-bind="row.title"></div>
 						<div class="row-text pointer" ng-click="toggle_row(row)" ng-bind="row.title"></div>
 					</div>
 					</div>
 					<div class="row-open" ng-show="!row.collapse">
 					<div class="row-open" ng-show="!row.collapse">
-						<div class='row-tab bgSuccess dropdown' ng-show="row.editable">
+						<div class='row-tab bgSuccess dropdown' ng-show="dashboardMeta.canEdit">
 							<span class="row-tab-button dropdown-toggle" data-toggle="dropdown">
 							<span class="row-tab-button dropdown-toggle" data-toggle="dropdown">
 								<i class="fa fa-bars"></i>
 								<i class="fa fa-bars"></i>
 							</span>
 							</span>
@@ -99,7 +99,7 @@
 			</div>
 			</div>
 		</div>
 		</div>
 
 
-		<div ng-show='dashboard.editable' class="row-fluid add-row-panel-hint">
+		<div ng-show='dashboardMeta.canEdit' class="row-fluid add-row-panel-hint">
 			<div class="span12" style="text-align:right;">
 			<div class="span12" style="text-align:right;">
 				<span style="margin-right: 10px;" ng-click="add_row_default()" class="pointer btn btn-info btn-small">
 				<span style="margin-right: 10px;" ng-click="add_row_default()" class="pointer btn btn-info btn-small">
 					<span><i class="fa fa-plus"></i> ADD ROW</span>
 					<span><i class="fa fa-plus"></i> ADD ROW</span>

+ 100 - 45
public/app/partials/dasheditor.html

@@ -17,63 +17,118 @@
 
 
 </div>
 </div>
 
 
-<div class="gf-box-body">
-
-		<div ng-if="editor.index == 0">
-			<div class="editor-row">
-				<div class="section">
-					<div class="editor-option">
-						<label class="small">Title</label><input type="text" class="input-large" ng-model='dashboard.title'></input>
-					</div>
-					<div class="editor-option">
-						<label class="small">Time correction</label>
-						<select ng-model="dashboard.timezone" class='input-small' ng-options="f for f in ['browser','utc']"></select>
-					</div>
-					<editor-opt-bool text="Hide controls (CTRL+H)" model="dashboard.hideControls"></editor-opt-bool>
-          <editor-opt-bool text="Shared Crosshair (CTRL+O)" model="dashboard.sharedCrosshair"></editor-opt-bool>
+<div class="gf-box-body" style="padding-bottom: 50px;">
+	<div ng-if="editor.index == 0">
+		<div class="editor-row">
+			<div class="section">
+				<h5>Dashboard info</h5>
+				<div class="tight-form">
+					<ul class="tight-form-list">
+						<li class="tight-form-item" style="width: 90px">
+							Title
+						</li>
+						<li>
+							<input type="text" class="input-xlarge tight-form-input" ng-model='dashboard.title'></input>
+						</li>
+						<li class="tight-form-item">
+							Tags
+							<tip>Press enter to a add tag</tip>
+						</li>
+						<li>
+							<bootstrap-tagsinput ng-model="dashboard.tags" tagclass="label label-tag" placeholder="add tags">
+							</bootstrap-tagsinput>
+						</li>
+					</ul>
+					<div class="clearfix"></div>
 				</div>
 				</div>
-			</div>
-			<div class="editor-row">
-				<div class="section">
-					<div class="editor-option">
-						<label class="small">Tags</label>
-						<bootstrap-tagsinput ng-model="dashboard.tags" tagclass="label label-tag" placeholder="add tags">
-						</bootstrap-tagsinput>
-						<tip>Press enter to a add tag</tip>
-					</div>
+				<div class="tight-form">
+					<ul class="tight-form-list">
+						<li class="tight-form-item" style="width: 90px">
+							Timezone
+						</li>
+						<li>
+							<select ng-model="dashboard.timezone" class='input-small tight-form-input' ng-options="f for f in ['browser','utc']"></select>
+						</li>
+					</ul>
+					<div class="clearfix"></div>
 				</div>
 				</div>
+
 			</div>
 			</div>
-		</div>
 
 
-		<div ng-if="editor.index == 1">
-			<div class="editor-row">
-				<div class="span6">
-					<table class="grafana-options-table">
-						<tr ng-repeat="row in dashboard.rows">
-							<td style="width: 97%">
-								{{row.title}}
-							</td>
-							<td><i ng-click="_.move(dashboard.rows,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
-							<td><i ng-click="_.move(dashboard.rows,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
-							<td>
-								<a ng-click="dashboard.rows = _.without(dashboard.rows,row)" class="btn btn-danger btn-small">
-									<i class="fa fa-remove"></i>
-								</a>
-							</td>
-						</tr>
-					</table>
+			<div class="section">
+				<h5>Toggles</h5>
+				<div class="tight-form">
+					<ul class="tight-form-list">
+						<li class="tight-form-item" style="width: 181px">
+							<label class="checkbox-label" for="dashboard.editable">Editable</label>
+						</li>
+						<li>
+							<li class="tight-form-item last">
+								<input class="cr1" id="dashboard.editable" type="checkbox" ng-model="dashboard.editable" ng-checked="dashboard.editable">
+								<label for="dashboard.editable" class="cr1"></label>
+							</li>
+						</li>
+					</ul>
+					<div class="clearfix"></div>
+				</div>
+				<div class="tight-form">
+					<ul class="tight-form-list">
+						<li class="tight-form-item" style="width: 181px">
+							<label class="checkbox-label" for="dashboard.hideControls">Hide Controls (CTRL+H)</label>
+						</li>
+						<li class="tight-form-item last">
+							<input class="cr1" id="dashboard.hideControls" type="checkbox" ng-model="dashboard.hideControls" ng-checked="dashboard.hideControls">
+							<label for="dashboard.hideControls" class="cr1"></label>
+						</li>
+					</ul>
+					<div class="clearfix"></div>
+				</div>
+				<div class="tight-form">
+					<ul class="tight-form-list">
+						<li class="tight-form-item" style="width: 181px">
+							<label class="checkbox-label" for="dashboard.sharedCrosshair">Shared Crosshair (CTRL+H)</label>
+						</li>
+						<li class="tight-form-item last">
+							<input class="cr1" id="dashboard.sharedCrosshair" type="checkbox" ng-model="dashboard.sharedCrosshair" ng-checked="dashboard.sharedCrosshair">
+							<label for="dashboard.sharedCrosshair" class="cr1"></label>
+						</li>
+					</ul>
+					<div class="clearfix"></div>
 				</div>
 				</div>
 			</div>
 			</div>
 		</div>
 		</div>
+	</div>
 
 
-		<div ng-repeat="pulldown in dashboard.nav" ng-controller="SubmenuCtrl" ng-show="editor.index == 2+$index">
-			<ng-include ng-show="pulldown.enable" src="pulldownEditorPath(pulldown.type)"></ng-include>
-			<button ng-hide="pulldown.enable" class="btn" ng-click="pulldown.enable = true">Enable the {{pulldown.type}}</button>
+	<div ng-if="editor.index == 1">
+		<div class="editor-row">
+			<div class="span6">
+				<table class="grafana-options-table">
+					<tr ng-repeat="row in dashboard.rows">
+						<td style="width: 97%">
+							{{row.title}}
+						</td>
+						<td><i ng-click="_.move(dashboard.rows,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
+						<td><i ng-click="_.move(dashboard.rows,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
+						<td>
+							<a ng-click="dashboard.rows = _.without(dashboard.rows,row)" class="btn btn-danger btn-small">
+								<i class="fa fa-remove"></i>
+							</a>
+						</td>
+					</tr>
+				</table>
+			</div>
+			<div class="clearfix"></div>
 		</div>
 		</div>
+	</div>
 
 
+	<div ng-repeat="pulldown in dashboard.nav" ng-controller="SubmenuCtrl" ng-show="editor.index == 2+$index">
+		<ng-include ng-show="pulldown.enable" src="pulldownEditorPath(pulldown.type)"></ng-include>
+		<button ng-hide="pulldown.enable" class="btn" ng-click="pulldown.enable = true">Enable the {{pulldown.type}}</button>
 	</div>
 	</div>
 
 
-	<div class="clearfix"></div>
+</div>
+
+<div class="clearfix"></div>
 </div>
 </div>
 
 
 <div class="gf-box-footer">
 <div class="gf-box-footer">

+ 1 - 1
public/app/plugins/datasource/elasticsearch/datasource.js

@@ -74,7 +74,7 @@ function (angular, _, config, kbn, moment) {
       var data = {
       var data = {
         "fields": [timeField, "_source"],
         "fields": [timeField, "_source"],
         "query" : { "filtered": { "query" : query, "filter": filter } },
         "query" : { "filtered": { "query" : query, "filter": filter } },
-        "size": 100
+        "size": 10000
       };
       };
 
 
       return this._request('POST', '/_search', annotation.index, data).then(function(results) {
       return this._request('POST', '/_search', annotation.index, data).then(function(results) {

+ 2 - 3
public/app/plugins/datasource/grafana/partials/query.editor.html

@@ -6,12 +6,11 @@
 
 
 		<p>
 		<p>
 		This is just a test data source that generates random walk series. If this is your only data source
 		This is just a test data source that generates random walk series. If this is your only data source
-		open the left side menu and navigate to the data sources admin screen and add your data sources. You can change
-		data source using the button to the left of the <strong>Add query</strong> button.
+		open the left side menu and navigate to the data sources admin screen and add your data sources (you need to be
+		logged in to do this). You can change data source using the button to the left of the <strong>Add query</strong> button.
 		</p>
 		</p>
 	</div>
 	</div>
 	<div class="span2"></div>
 	<div class="span2"></div>
 
 
 	<div class="clearfix"></div>
 	<div class="clearfix"></div>
 </div>
 </div>
-

+ 38 - 18
public/app/routes/dashLoadControllers.js

@@ -18,6 +18,8 @@ function (angular, _, kbn, moment, $) {
 
 
     if (!$routeParams.slug) {
     if (!$routeParams.slug) {
       backendSrv.get('/api/dashboards/home').then(function(result) {
       backendSrv.get('/api/dashboards/home').then(function(result) {
+        var meta = result.meta;
+        meta.canSave = meta.canShare = meta.canEdit = meta.canStar = false;
         $scope.initDashboard(result, $scope);
         $scope.initDashboard(result, $scope);
       },function() {
       },function() {
         dashboardLoadFailed('Not found');
         dashboardLoadFailed('Not found');
@@ -38,7 +40,16 @@ function (angular, _, kbn, moment, $) {
     backendSrv.get('/api/snapshots/' + $routeParams.key).then(function(result) {
     backendSrv.get('/api/snapshots/' + $routeParams.key).then(function(result) {
       $scope.initDashboard(result, $scope);
       $scope.initDashboard(result, $scope);
     }, function() {
     }, function() {
-      $scope.initDashboard({meta: {isSnapshot: true}, model: {title: 'Snapshot not found'}}, $scope);
+      $scope.initDashboard({
+        meta: {
+          isSnapshot: true,
+          canSave: false,
+          canEdit: false,
+        },
+        model: {
+          title: 'Snapshot not found'
+        }
+      }, $scope);
     });
     });
   });
   });
 
 
@@ -48,15 +59,18 @@ function (angular, _, kbn, moment, $) {
       $location.path('');
       $location.path('');
       return;
       return;
     }
     }
-    $scope.initDashboard({meta: {}, model: window.grafanaImportDashboard }, $scope);
+    $scope.initDashboard({
+      meta: { canShare: false, canStar: false },
+      model: window.grafanaImportDashboard
+    }, $scope);
   });
   });
 
 
   module.controller('NewDashboardCtrl', function($scope) {
   module.controller('NewDashboardCtrl', function($scope) {
     $scope.initDashboard({
     $scope.initDashboard({
-      meta: {},
+      meta: { canStar: false, canShare: false },
       model: {
       model: {
         title: "New dashboard",
         title: "New dashboard",
-      rows: [{ height: '250px', panels:[] }]
+        rows: [{ height: '250px', panels:[] }]
       },
       },
     }, $scope);
     }, $scope);
   });
   });
@@ -66,10 +80,10 @@ function (angular, _, kbn, moment, $) {
     var file_load = function(file) {
     var file_load = function(file) {
       return $http({
       return $http({
         url: "public/dashboards/"+file.replace(/\.(?!json)/,"/")+'?' + new Date().getTime(),
         url: "public/dashboards/"+file.replace(/\.(?!json)/,"/")+'?' + new Date().getTime(),
-             method: "GET",
-             transformResponse: function(response) {
-               return angular.fromJson(response);
-             }
+        method: "GET",
+        transformResponse: function(response) {
+          return angular.fromJson(response);
+        }
       }).then(function(result) {
       }).then(function(result) {
         if(!result) {
         if(!result) {
           return false;
           return false;
@@ -82,7 +96,10 @@ function (angular, _, kbn, moment, $) {
     };
     };
 
 
     file_load($routeParams.jsonFile).then(function(result) {
     file_load($routeParams.jsonFile).then(function(result) {
-      $scope.initDashboard({meta: {fromFile: true}, model: result}, $scope);
+      $scope.initDashboard({
+        meta: { canSave: false, canDelete: false },
+        model: result
+      }, $scope);
     });
     });
 
 
   });
   });
@@ -92,8 +109,8 @@ function (angular, _, kbn, moment, $) {
     var execute_script = function(result) {
     var execute_script = function(result) {
       var services = {
       var services = {
         dashboardSrv: dashboardSrv,
         dashboardSrv: dashboardSrv,
-    datasourceSrv: datasourceSrv,
-    $q: $q,
+        datasourceSrv: datasourceSrv,
+        $q: $q,
       };
       };
 
 
       /*jshint -W054 */
       /*jshint -W054 */
@@ -118,16 +135,19 @@ function (angular, _, kbn, moment, $) {
       var url = 'public/dashboards/'+file.replace(/\.(?!js)/,"/") + '?' + new Date().getTime();
       var url = 'public/dashboards/'+file.replace(/\.(?!js)/,"/") + '?' + new Date().getTime();
 
 
       return $http({ url: url, method: "GET" })
       return $http({ url: url, method: "GET" })
-        .then(execute_script)
-        .then(null,function(err) {
-          console.log('Script dashboard error '+ err);
-          $scope.appEvent('alert-error', ["Script Error", "Please make sure it exists and returns a valid dashboard"]);
-          return false;
-        });
+      .then(execute_script)
+      .then(null,function(err) {
+        console.log('Script dashboard error '+ err);
+        $scope.appEvent('alert-error', ["Script Error", "Please make sure it exists and returns a valid dashboard"]);
+        return false;
+      });
     };
     };
 
 
     script_load($routeParams.jsFile).then(function(result) {
     script_load($routeParams.jsFile).then(function(result) {
-      $scope.initDashboard({meta: {fromScript: true}, model: result.data}, $scope);
+      $scope.initDashboard({
+        meta: {fromScript: true, canDelete: false, canSave: false},
+        model: result.data
+      }, $scope);
     });
     });
 
 
   });
   });

+ 2 - 1
public/app/services/backendSrv.js

@@ -63,8 +63,9 @@ function (angular, _, config) {
       var requestIsLocal = options.url.indexOf('/') === 0;
       var requestIsLocal = options.url.indexOf('/') === 0;
       var firstAttempt = options.retry === 0;
       var firstAttempt = options.retry === 0;
 
 
-      if (requestIsLocal && firstAttempt) {
+      if (requestIsLocal && !options.hasSubUrl) {
         options.url = config.appSubUrl + options.url;
         options.url = config.appSubUrl + options.url;
+        options.hasSubUrl = true;
       }
       }
 
 
       return $http(options).then(function(results) {
       return $http(options).then(function(results) {

+ 7 - 8
public/app/services/contextSrv.js

@@ -18,13 +18,6 @@ function (angular, _, store, config) {
       }
       }
     }
     }
 
 
-    this.version = config.buildInfo.version;
-    this.lightTheme = false;
-    this.user = new User();
-    this.isSignedIn = this.user.isSignedIn;
-    this.isGrafanaAdmin = this.user.isGrafanaAdmin;
-    this.sidemenu = store.getBool('grafana.sidemenu');
-
     // events
     // events
     $rootScope.$on('toggle-sidemenu', function() {
     $rootScope.$on('toggle-sidemenu', function() {
       self.toggleSideMenu();
       self.toggleSideMenu();
@@ -47,6 +40,12 @@ function (angular, _, store, config) {
       }, 50);
       }, 50);
     };
     };
 
 
+    this.version = config.buildInfo.version;
+    this.lightTheme = false;
+    this.user = new User();
+    this.isSignedIn = this.user.isSignedIn;
+    this.isGrafanaAdmin = this.user.isGrafanaAdmin;
+    this.sidemenu = store.getBool('grafana.sidemenu');
+    this.isEditor = this.hasRole('Editor') || this.hasRole('Admin');
   });
   });
-
 });
 });

+ 8 - 22
public/css/less/bootstrap-tagsinput.less

@@ -1,33 +1,19 @@
 .bootstrap-tagsinput {
 .bootstrap-tagsinput {
   display: inline-block;
   display: inline-block;
-  padding: 4px 6px;
-  margin-bottom: 10px;
-  color: #555;
+  padding: 0 0 0 6px;
   vertical-align: middle;
   vertical-align: middle;
-  border-radius: 4px;
   max-width: 100%;
   max-width: 100%;
   line-height: 22px;
   line-height: 22px;
-
   background-color: @inputBackground;
   background-color: @inputBackground;
-  border: 1px solid @inputBorder;
-  .box-shadow(inset 0 1px 1px rgba(0,0,0,.075));
-  .transition(~"border linear .2s, box-shadow linear .2s");
 
 
   input {
   input {
     border: none;
     border: none;
-    box-shadow: none;
-    outline: none;
-    background-color: transparent;
-    padding: 0;
-    padding-left: 5px;
-    margin: 0;
-    width: auto !important;
-    max-width: inherit;
-
-    &:focus {
-      border: none;
-      box-shadow: none;
-    }
+    border-right: 1px solid @grafanaTargetSegmentBorder;
+    margin: 0px;
+    border-radius: 0;
+    padding: 8px 6px;
+    height: 100%;
+    box-sizing: border-box;
   }
   }
 
 
   .tag {
   .tag {
@@ -49,4 +35,4 @@
       }
       }
     }
     }
   }
   }
-}
+}

+ 2 - 1
public/css/less/search.less

@@ -22,7 +22,8 @@
   padding-bottom: 10px;
   padding-bottom: 10px;
   input {
   input {
     width: 100%;
     width: 100%;
-    padding: 18px 8px;
+    padding: 8px 8px;
+    height: 100%;
     box-sizing: border-box;
     box-sizing: border-box;
   }
   }
   button {
   button {

+ 18 - 2
public/test/specs/dashboardSrv-specs.js

@@ -185,10 +185,26 @@ define([
         expect(model.annotations.list.length).to.be(0);
         expect(model.annotations.list.length).to.be(0);
         expect(model.templating.list.length).to.be(0);
         expect(model.templating.list.length).to.be(0);
       });
       });
-
     });
     });
 
 
-  });
+    describe('Given editable false dashboard', function() {
+      var model;
 
 
+      beforeEach(function() {
+        model = _dashboardSrv.create({
+          editable:  false,
+        });
+      });
 
 
+      it('Should set meta canEdit and canSave to false', function() {
+        expect(model.meta.canSave).to.be(false);
+        expect(model.meta.canEdit).to.be(false);
+      });
+
+      it('getSaveModelClone should remove meta', function() {
+        var clone = model.getSaveModelClone();
+        expect(clone.meta).to.be(undefined);
+      });
+    });
+  });
 });
 });

+ 1 - 1
public/vendor/tagsinput/bootstrap-tagsinput.js

@@ -500,4 +500,4 @@
   $(function() {
   $(function() {
     $("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput();
     $("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput();
   });
   });
-})(window.jQuery);
+})(window.jQuery);

+ 1 - 1
tasks/options/compress.js

@@ -16,7 +16,7 @@ module.exports = function(config) {
         {
         {
           expand: true,
           expand: true,
           src: ['LICENSE.md', 'README.md', 'NOTICE.md'],
           src: ['LICENSE.md', 'README.md', 'NOTICE.md'],
-          dest: '<%= pkg.name %>/',
+          dest: '<%= pkg.name %>-<%= pkg.version %>/',
         }
         }
       ]
       ]
     }
     }