diff --git a/CHANGELOG.md b/CHANGELOG.md index acbe07795be..70aaa67ab22 100644 --- a/CHANGELOG.md +++ b/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** - [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 diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index a7e150e215e..dd08ac91121 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,6 +1,6 @@ { "ImportPath": "github.com/grafana/grafana", - "GoVersion": "go1.4.2", + "GoVersion": "go1.3", "Packages": [ "./pkg/..." ], @@ -13,6 +13,14 @@ "ImportPath": "github.com/Unknwon/macaron", "Rev": "93de4f3fad97bf246b838f828e2348f46f21f20a" }, + { + "ImportPath": "github.com/dalu/slug", + "Rev": "6dbd13912e9be466e2c1de349a2c7d1466c97e07" + }, + { + "ImportPath": "github.com/dalu/unidecode", + "Rev": "339814d47f3e32a6f7036a0a4c56ed9b373dd755" + }, { "ImportPath": "github.com/go-sql-driver/mysql", "Comment": "v1.2-26-g9543750", @@ -27,10 +35,6 @@ "Comment": "v0.4.2-58-ge2889e5", "Rev": "e2889e5517600b82905f1d2ba8b70deb71823ffe" }, - { - "ImportPath": "github.com/gosimple/slug", - "Rev": "a2392a4a87fa0366cbff131d3fd421f83f52492f" - }, { "ImportPath": "github.com/jtolds/gls", "Rev": "f1ac7f4f24f50328e6bc838ca4437d1612a0243c" @@ -87,10 +91,6 @@ { "ImportPath": "gopkgs.com/pool.v1", "Rev": "c850f092aad1780cbffff25f471c5cc32097932a" - }, - { - "ImportPath": "gopkgs.com/unidecode.v1", - "Rev": "4deae2c05236b41cc39f8144ac87a837ba974d40" } ] } diff --git a/Godeps/_workspace/src/github.com/gosimple/slug/README.md b/Godeps/_workspace/src/github.com/dalu/slug/README.md similarity index 72% rename from Godeps/_workspace/src/github.com/gosimple/slug/README.md rename to Godeps/_workspace/src/github.com/dalu/slug/README.md index a2649bdb170..ddefc36ff04 100644 --- a/Godeps/_workspace/src/github.com/gosimple/slug/README.md +++ b/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 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 @@ -38,12 +37,9 @@ multiple languages support. fmt.Println(textSub) // Will print 'sand-is-hot' } -### Requests or bugs? - - ## Installation - go get -u github.com/gosimple/slug + go get -u github.com/dalu/slug ## License diff --git a/Godeps/_workspace/src/github.com/gosimple/slug/default_substitution.go b/Godeps/_workspace/src/github.com/dalu/slug/default_substitution.go similarity index 100% rename from Godeps/_workspace/src/github.com/gosimple/slug/default_substitution.go rename to Godeps/_workspace/src/github.com/dalu/slug/default_substitution.go diff --git a/Godeps/_workspace/src/github.com/gosimple/slug/doc.go b/Godeps/_workspace/src/github.com/dalu/slug/doc.go similarity index 91% rename from Godeps/_workspace/src/github.com/gosimple/slug/doc.go rename to Godeps/_workspace/src/github.com/dalu/slug/doc.go index ffbe2c223f5..39f57b30eb4 100644 --- a/Godeps/_workspace/src/github.com/gosimple/slug/doc.go +++ b/Godeps/_workspace/src/github.com/dalu/slug/doc.go @@ -12,7 +12,7 @@ Example: package main import( - "github.com/gosimple/slug" + "github.com/dalu/slug" "fmt" ) @@ -35,9 +35,5 @@ Example: textSub := slug.Make("water is hot") fmt.Println(textSub) // Will print 'sand-is-hot' } - -Requests or bugs? - -https://github.com/gosimple/slug/issues */ package slug diff --git a/Godeps/_workspace/src/github.com/gosimple/slug/languages_substitution.go b/Godeps/_workspace/src/github.com/dalu/slug/languages_substitution.go similarity index 100% rename from Godeps/_workspace/src/github.com/gosimple/slug/languages_substitution.go rename to Godeps/_workspace/src/github.com/dalu/slug/languages_substitution.go diff --git a/Godeps/_workspace/src/github.com/gosimple/slug/slug.go b/Godeps/_workspace/src/github.com/dalu/slug/slug.go similarity index 99% rename from Godeps/_workspace/src/github.com/gosimple/slug/slug.go rename to Godeps/_workspace/src/github.com/dalu/slug/slug.go index 26974fbd7d8..85d614f941e 100644 --- a/Godeps/_workspace/src/github.com/gosimple/slug/slug.go +++ b/Godeps/_workspace/src/github.com/dalu/slug/slug.go @@ -6,7 +6,7 @@ package slug import ( - "gopkgs.com/unidecode.v1" + "github.com/dalu/unidecode" "regexp" "strings" ) diff --git a/Godeps/_workspace/src/github.com/gosimple/slug/slug_test.go b/Godeps/_workspace/src/github.com/dalu/slug/slug_test.go similarity index 100% rename from Godeps/_workspace/src/github.com/gosimple/slug/slug_test.go rename to Godeps/_workspace/src/github.com/dalu/slug/slug_test.go diff --git a/Godeps/_workspace/src/gopkgs.com/unidecode.v1/.gitignore b/Godeps/_workspace/src/github.com/dalu/unidecode/.gitignore similarity index 100% rename from Godeps/_workspace/src/gopkgs.com/unidecode.v1/.gitignore rename to Godeps/_workspace/src/github.com/dalu/unidecode/.gitignore diff --git a/Godeps/_workspace/src/gopkgs.com/unidecode.v1/LICENSE b/Godeps/_workspace/src/github.com/dalu/unidecode/LICENSE similarity index 100% rename from Godeps/_workspace/src/gopkgs.com/unidecode.v1/LICENSE rename to Godeps/_workspace/src/github.com/dalu/unidecode/LICENSE diff --git a/Godeps/_workspace/src/gopkgs.com/unidecode.v1/README.md b/Godeps/_workspace/src/github.com/dalu/unidecode/README.md similarity index 65% rename from Godeps/_workspace/src/gopkgs.com/unidecode.v1/README.md rename to Godeps/_workspace/src/github.com/dalu/unidecode/README.md index fa5d70841fb..589d955593c 100644 --- a/Godeps/_workspace/src/gopkgs.com/unidecode.v1/README.md +++ b/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. -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 diff --git a/Godeps/_workspace/src/gopkgs.com/unidecode.v1/decode.go b/Godeps/_workspace/src/github.com/dalu/unidecode/decode.go similarity index 100% rename from Godeps/_workspace/src/gopkgs.com/unidecode.v1/decode.go rename to Godeps/_workspace/src/github.com/dalu/unidecode/decode.go diff --git a/Godeps/_workspace/src/gopkgs.com/unidecode.v1/make_table.go b/Godeps/_workspace/src/github.com/dalu/unidecode/make_table.go similarity index 100% rename from Godeps/_workspace/src/gopkgs.com/unidecode.v1/make_table.go rename to Godeps/_workspace/src/github.com/dalu/unidecode/make_table.go diff --git a/Godeps/_workspace/src/gopkgs.com/unidecode.v1/table.go b/Godeps/_workspace/src/github.com/dalu/unidecode/table.go similarity index 100% rename from Godeps/_workspace/src/gopkgs.com/unidecode.v1/table.go rename to Godeps/_workspace/src/github.com/dalu/unidecode/table.go diff --git a/Godeps/_workspace/src/gopkgs.com/unidecode.v1/table.txt b/Godeps/_workspace/src/github.com/dalu/unidecode/table.txt similarity index 100% rename from Godeps/_workspace/src/gopkgs.com/unidecode.v1/table.txt rename to Godeps/_workspace/src/github.com/dalu/unidecode/table.txt diff --git a/Godeps/_workspace/src/gopkgs.com/unidecode.v1/unidecode.go b/Godeps/_workspace/src/github.com/dalu/unidecode/unidecode.go similarity index 100% rename from Godeps/_workspace/src/gopkgs.com/unidecode.v1/unidecode.go rename to Godeps/_workspace/src/github.com/dalu/unidecode/unidecode.go diff --git a/Godeps/_workspace/src/gopkgs.com/unidecode.v1/unidecode_test.go b/Godeps/_workspace/src/github.com/dalu/unidecode/unidecode_test.go similarity index 100% rename from Godeps/_workspace/src/gopkgs.com/unidecode.v1/unidecode_test.go rename to Godeps/_workspace/src/github.com/dalu/unidecode/unidecode_test.go diff --git a/Godeps/_workspace/src/github.com/gosimple/slug/.gitignore b/Godeps/_workspace/src/github.com/gosimple/slug/.gitignore deleted file mode 100644 index 02a8da53752..00000000000 --- a/Godeps/_workspace/src/github.com/gosimple/slug/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -_* -cover*.out diff --git a/Godeps/_workspace/src/gopkgs.com/unidecode.v1/gopkgs.go b/Godeps/_workspace/src/gopkgs.com/unidecode.v1/gopkgs.go deleted file mode 100644 index a907de14384..00000000000 --- a/Godeps/_workspace/src/gopkgs.com/unidecode.v1/gopkgs.go +++ /dev/null @@ -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)) - } -} diff --git a/Gruntfile.js b/Gruntfile.js index 9303913733d..43cb1f09bd1 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,7 +1,6 @@ /* jshint node:true */ 'use strict'; module.exports = function (grunt) { - var os = require('os'); var config = { pkg: grunt.file.readJSON('package.json'), @@ -13,6 +12,10 @@ module.exports = function (grunt) { 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; // load plugins @@ -35,7 +38,6 @@ module.exports = function (grunt) { // Merge that object with what with whatever we have here loadConfig(config,'./tasks/options/'); - // pass the config to grunt grunt.initConfig(config); }; diff --git a/README.md b/README.md index c0b4f128c77..bd178eddbdd 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ bra run ### Running ``` -./grafana web +./grafana ``` Open grafana in your browser (default http://localhost:3000) and login with admin user (default user/pass = admin/admin). diff --git a/build.go b/build.go index d7b53d7ea3a..d14b1458586 100644 --- a/build.go +++ b/build.go @@ -22,13 +22,16 @@ import ( ) 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 @@ -40,7 +43,7 @@ func main() { ensureGoPath() 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(&goos, "goos", runtime.GOOS, "GOOS") @@ -70,7 +73,7 @@ func main() { case "package": //verifyGitRepoIsClean() - grunt("release", "--pkgVer="+version) + grunt("release") createLinuxPackages() case "latest": @@ -107,6 +110,16 @@ func readVersionFromPackageJson() { } 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 { @@ -208,10 +221,14 @@ func createPackage(options linuxPackageOptions) { "--config-files", options.systemdServiceFilePath, "--after-install", options.postinstSrc, "--name", "grafana", - "--version", version, + "--version", linuxPackageVersion, "-p", "./dist", } + if linuxPackageIteration != "" { + args = append(args, "--iteration", linuxPackageIteration) + } + // add dependenciesj for _, dep := range options.depends { args = append(args, "--depends", dep) @@ -259,6 +276,7 @@ func grunt(params ...string) { func setup() { 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", "install", "-v", "github.com/mattn/go-sqlite3") } diff --git a/conf/defaults.ini b/conf/defaults.ini index 042f1a5574c..7345f18520e 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -7,7 +7,7 @@ app_mode = production #################################### 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 # @@ -62,7 +62,7 @@ path = grafana.db #################################### Session #################################### [session] -# Either "memory", "file", "redis", "mysql", default is "memory" +# Either "memory", "file", "redis", "mysql", "postgresql", default is "file" provider = file # Provider config options @@ -70,6 +70,7 @@ provider = file # 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` # mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1)/database_name` + provider_config = sessions # Session cookie name @@ -139,6 +140,7 @@ enabled = false client_id = some_id client_secret = some_secret scopes = user:email +team_ids = auth_url = https://github.com/login/oauth/authorize token_url = https://github.com/login/oauth/access_token api_url = https://api.github.com/user diff --git a/conf/sample.ini b/conf/sample.ini index 6aaa69f7314..4ebab72d1af 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -7,7 +7,7 @@ #################################### 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 # @@ -62,7 +62,7 @@ #################################### Session #################################### [session] -# Either "memory", "file", "redis", "mysql", default is "memory" +# Either "memory", "file", "redis", "mysql", "postgresql", default is "file" ;provider = file # Provider config options @@ -142,8 +142,8 @@ ;auth_url = https://github.com/login/oauth/authorize ;token_url = https://github.com/login/oauth/access_token ;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 ########################## [auth.google] @@ -154,8 +154,7 @@ ;auth_url = https://accounts.google.com/o/oauth2/auth ;token_url = https://accounts.google.com/o/oauth2/token ;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 ########################## [log] diff --git a/docs/Makefile b/docs/Makefile index fcb1708f916..d44bc545e2c 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -44,7 +44,7 @@ docs-test: docs-build $(DOCKER_RUN_DOCS) "$(DOCKER_DOCS_IMAGE)" ./test.sh 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 "$(GITCOMMIT)" > GITCOMMIT docker build -t "$(DOCKER_DOCS_IMAGE)" . diff --git a/docs/sources/index.md b/docs/sources/index.md index ece3285d284..24ba087575e 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -4,9 +4,9 @@ page_keywords: grafana, introduction, documentation, about # 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. @@ -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 -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. @@ -35,4 +35,4 @@ If you have any trouble with Grafana, whether you can't get it set up or you jus ## 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. diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index c9a9bc14ab4..a1dfb6ac035 100644 --- a/docs/sources/installation/configuration.md +++ b/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_secret = YOUR_GITHUB_APP_CLIENT_SECRET scopes = user:email + team_ids = auth_url = https://github.com/login/oauth/authorize token_url = https://github.com/login/oauth/access_token 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 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] 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: @@ -219,7 +235,7 @@ set to true, any user successfully authenticating via google auth will be automa ## [session] ### provider -Valid values are "memory", "file", "mysql", 'postgres'. Default is "memory". +Valid values are "memory", "file", "mysql", 'postgres'. Default is "file". ### provider_config 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. 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 -enabled. Counters are sent every 24 hours. +enabled. Counters are sent every 24 hours. Default value is `true`. ### google_analytics_ua_id If you want to track Grafana usage via Google analytics specify *your* Univeral Analytics ID here. By defualt this feature is disabled. - - diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index aed4eb420b5..c2594e38445 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -10,11 +10,11 @@ page_keywords: grafana, installation, debian, ubuntu, guide 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 - $ 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 dpkg -i grafana_2.0.2_amd64.deb diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 1f8277a4907..9b7dc0ed9ed 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -10,12 +10,12 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide 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 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` @@ -30,9 +30,9 @@ Add the following to a new file at `/etc/yum.repos.d/grafana.repo` name=grafana baseurl=https://packagecloud.io/grafana/stable/el/6/$basearch repo_gpgcheck=1 - gpgcheck=0 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 sslcacert=/etc/pki/tls/certs/ca-bundle.crt diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index e2acb76311a..5bce40b3233 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -6,8 +6,23 @@ page_keywords: grafana, installation, windows guide # 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 diff --git a/docs/sources/reference/http_api.md b/docs/sources/reference/http_api.md index b7ef92e0ba8..f02741cb37b 100644 --- a/docs/sources/reference/http_api.md +++ b/docs/sources/reference/http_api.md @@ -6,5 +6,147 @@ page_keywords: grafana, admin, http, api, documentation # 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 `. + +## 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 + diff --git a/docs/sources/reference/timerange.md b/docs/sources/reference/timerange.md index 4f5f4659594..47c9186e119 100644 --- a/docs/sources/reference/timerange.md +++ b/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. -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) diff --git a/latest.json b/latest.json index 7f8c221daf3..a6613c11634 100644 --- a/latest.json +++ b/latest.json @@ -1,3 +1,3 @@ { - "version": "2.0.0-beta3", + "version": "2.0.1", } diff --git a/package.json b/package.json index ace54af3be9..8785632eaee 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Coding Instinct AB" }, "name": "grafana", - "version": "2.0.1", + "version": "2.1.0-pre1", "repository": { "type": "git", "url": "http://github.com/torkelo/grafana.git" diff --git a/packaging/deb/init.d/grafana-server b/packaging/deb/init.d/grafana-server index 2e98f12aece..6daebdb4331 100755 --- a/packaging/deb/init.d/grafana-server +++ b/packaging/deb/init.d/grafana-server @@ -87,6 +87,7 @@ case "$1" in # check if pid file has been written two if ! [[ -s $PID_FILE ]]; then log_end_msg 1 + exit 1 fi i=0 @@ -96,7 +97,10 @@ case "$1" in do sleep 1 i=$(($i + 1)) - [ $i -gt $timeout ] && log_end_msg 1 + if [ $i -gt $timeout ]; then + log_end_msg 1 + exit 1 + fi done fi log_end_msg $return diff --git a/packaging/rpm/init.d/grafana-server b/packaging/rpm/init.d/grafana-server index 96e0c18c5e4..a30fffb6e00 100755 --- a/packaging/rpm/init.d/grafana-server +++ b/packaging/rpm/init.d/grafana-server @@ -144,5 +144,3 @@ case "$1" in exit 1 ;; esac - -exit 0 diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 53519631d22..fea88f07550 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -21,16 +21,17 @@ type CurrentUser struct { Email string `json:"email"` Name string `json:"name"` LightTheme bool `json:"lightTheme"` - OrgRole m.RoleType `json:"orgRole"` + OrgId int64 `json:"orgId"` OrgName string `json:"orgName"` + OrgRole m.RoleType `json:"orgRole"` IsGrafanaAdmin bool `json:"isGrafanaAdmin"` GravatarUrl string `json:"gravatarUrl"` } 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"` Expires time.Time `json:"expires"` Created time.Time `json:"created"` diff --git a/pkg/api/index.go b/pkg/api/index.go index d9ecf65b699..86a5e3f1882 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -18,6 +18,7 @@ func setIndexViewData(c *middleware.Context) error { Email: c.Email, Name: c.Name, LightTheme: c.Theme == "light", + OrgId: c.OrgId, OrgName: c.OrgName, OrgRole: c.OrgRole, GravatarUrl: dtos.GetGravatarUrl(c.Email), diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 11d62754a18..505c17ddde8 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -3,6 +3,7 @@ package api import ( "errors" "fmt" + "net/url" "golang.org/x/oauth2" @@ -45,7 +46,11 @@ func OAuthLogin(ctx *middleware.Context) { userInfo, err := connect.UserInfo(token) 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 } @@ -54,7 +59,7 @@ func OAuthLogin(ctx *middleware.Context) { // validate that the email is allowed to login to grafana if !connect.IsEmailAllowed(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 } diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 7c4e8db1611..159d4e1a3c6 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -5,7 +5,7 @@ import ( "strings" "time" - "github.com/gosimple/slug" + "github.com/dalu/slug" ) // Typed errors diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index 6ded17ff0e7..5d440d85ebc 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -86,4 +86,10 @@ func addDashboardMigration(mg *Migrator) { })) 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;")) } diff --git a/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go b/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go index 4d83dfd5bc6..b08cc451e55 100644 --- a/pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go +++ b/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)) 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;")) } diff --git a/pkg/services/sqlstore/migrator/migrations.go b/pkg/services/sqlstore/migrator/migrations.go index e596ef6c171..a65c7ec7e81 100644 --- a/pkg/services/sqlstore/migrator/migrations.go +++ b/pkg/services/sqlstore/migrator/migrations.go @@ -25,8 +25,9 @@ func (m *MigrationBase) GetCondition() MigrationCondition { type RawSqlMigration struct { MigrationBase - sqlite string - mysql string + sqlite string + mysql string + postgres string } func (m *RawSqlMigration) Sql(dialect Dialect) string { @@ -35,6 +36,8 @@ func (m *RawSqlMigration) Sql(dialect Dialect) string { return m.mysql case SQLITE: return m.sqlite + case POSTGRES: + return m.postgres } panic("db type not supported") @@ -50,6 +53,11 @@ func (m *RawSqlMigration) Mysql(sql string) *RawSqlMigration { return m } +func (m *RawSqlMigration) Postgres(sql string) *RawSqlMigration { + m.postgres = sql + return m +} + type AddColumnMigration struct { MigrationBase tableName string diff --git a/pkg/social/social.go b/pkg/social/social.go index 47c7ea5dc38..355f85b54b6 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -2,7 +2,9 @@ package social import ( "encoding/json" + "errors" "fmt" + "net/http" "strconv" "strings" @@ -75,13 +77,24 @@ func NewOAuthService() { // GitHub. if name == "github" { 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. if name == "google" { 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 { *oauth2.Config allowedDomains []string - ApiUrl string + apiUrl string allowSignup bool + teamIds []int } +var ( + ErrMissingTeamMembership = errors.New("User not a member of one of the required teams") +) + func (s *SocialGithub) Type() int { return int(models.GITHUB) } @@ -119,6 +137,28 @@ func (s *SocialGithub) IsSignupAllowed() bool { 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) { var data struct { Id int `json:"id"` @@ -128,7 +168,7 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { var err error client := s.Client(oauth2.NoContext, token) - r, err := client.Get(s.ApiUrl) + r, err := client.Get(s.apiUrl) if err != nil { return nil, err } @@ -139,11 +179,23 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { return nil, err } - return &BasicUserInfo{ + userInfo := &BasicUserInfo{ Identity: strconv.Itoa(data.Id), Name: data.Name, 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 { *oauth2.Config allowedDomains []string - ApiUrl string + apiUrl string allowSignup bool } @@ -181,7 +233,7 @@ func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { var err error client := s.Client(oauth2.NoContext, token) - r, err := client.Get(s.ApiUrl) + r, err := client.Get(s.apiUrl) if err != nil { return nil, err } diff --git a/public/app/components/kbn.js b/public/app/components/kbn.js index 1193285787f..fa2b8214873 100644 --- a/public/app/components/kbn.js +++ b/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.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.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.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']); @@ -534,6 +537,7 @@ function($, _, moment) { {text: 'microseconds (µs)', value: 'µs'}, {text: 'milliseconds (ms)', value: 'ms'}, {text: 'seconds (s)', value: 's'}, + {text: 'Hertz (1/s)', value: 'hertz'}, ] }, { @@ -561,6 +565,8 @@ function($, _, moment) { {text: 'kilowatt-hour (kWh)', value: 'kwatth'}, {text: 'joule (J)', value: 'joule'}, {text: 'electron volt (eV)', value: 'ev'}, + {text: 'Ampere (A)', value: 'amp'}, + {text: 'Volt (V)', value: 'volt'}, ] }, { diff --git a/public/app/controllers/loginCtrl.js b/public/app/controllers/loginCtrl.js index 5de773842f8..c8856df0690 100644 --- a/public/app/controllers/loginCtrl.js +++ b/public/app/controllers/loginCtrl.js @@ -7,7 +7,7 @@ function (angular, config) { var module = angular.module('grafana.controllers'); - module.controller('LoginCtrl', function($scope, backendSrv, contextSrv) { + module.controller('LoginCtrl', function($scope, backendSrv, contextSrv, $location) { $scope.formModel = { user: '', email: '', @@ -28,6 +28,13 @@ function (angular, config) { $scope.init = function() { $scope.$watch("loginMode", $scope.loginModeChanged); $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 diff --git a/public/app/features/dashboard/dashboardNavCtrl.js b/public/app/features/dashboard/dashboardNavCtrl.js index 11595e30479..27ce528f69e 100644 --- a/public/app/features/dashboard/dashboardNavCtrl.js +++ b/public/app/features/dashboard/dashboardNavCtrl.js @@ -52,6 +52,10 @@ function (angular, _) { }; $scope.saveDashboard = function(options) { + if ($scope.dashboardMeta.canSave === false) { + return; + } + var clone = $scope.dashboard.getSaveModelClone(); backendSrv.saveDashboard(clone, options).then(function(data) { @@ -119,6 +123,8 @@ function (angular, _) { $scope.saveDashboardAs = function() { var newScope = $rootScope.$new(); newScope.clone = $scope.dashboard.getSaveModelClone(); + newScope.clone.editable = true; + newScope.clone.hideControls = false; $scope.appEvent('show-modal', { src: './app/features/dashboard/partials/saveDashboardAs.html', diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index 1064892745b..9324f18e33b 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -52,24 +52,22 @@ function (angular, $, kbn, _, moment) { p._initMeta = function(meta) { meta = meta || {}; - meta.canShare = true; - meta.canSave = true; - meta.canEdit = true; - meta.canStar = true; + + 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 (contextSrv.hasRole('Viewer')) { meta.canSave = false; } - if (meta.isSnapshot) { - meta.canSave = false; - } - - if (meta.isHome) { - meta.canShare = false; - meta.canStar = false; - meta.canSave = false; + if (!this.editable) { meta.canEdit = false; + meta.canDelete = false; + meta.canSave = false; + this.hideControls = true; } this.meta = meta; diff --git a/public/app/features/dashboard/partials/dashboardTopNav.html b/public/app/features/dashboard/partials/dashboardTopNav.html index 784185837e8..6f85d6b4e02 100644 --- a/public/app/features/dashboard/partials/dashboardTopNav.html +++ b/public/app/features/dashboard/partials/dashboardTopNav.html @@ -30,16 +30,16 @@
  • - diff --git a/public/app/features/dashboard/unsavedChangesSrv.js b/public/app/features/dashboard/unsavedChangesSrv.js index d17276dc374..eec884551ec 100644 --- a/public/app/features/dashboard/unsavedChangesSrv.js +++ b/public/app/features/dashboard/unsavedChangesSrv.js @@ -37,7 +37,7 @@ function(angular, _, config) { }); this.ignoreChanges = function() { - if (!self.current) { return true; } + if (!self.current || !self.current.meta) { return true; } var meta = self.current.meta; return !meta.canSave || meta.fromScript || meta.fromFile; diff --git a/public/app/features/panel/panelHelper.js b/public/app/features/panel/panelHelper.js index 442bcbac8c5..39d8e00a9d6 100644 --- a/public/app/features/panel/panelHelper.js +++ b/public/app/features/panel/panelHelper.js @@ -43,7 +43,7 @@ function (angular, _, kbn, $) { } if (scope.panel.timeShift) { - if (!kbn.isValidTimeSpan(scope.panel.timeFrom)) { + if (!kbn.isValidTimeSpan(scope.panel.timeShift)) { scope.panelMeta.timeInfo = 'invalid timeshift'; return; } diff --git a/public/app/features/panel/panelSrv.js b/public/app/features/panel/panelSrv.js index d8e2272436a..33f70ca25da 100644 --- a/public/app/features/panel/panelSrv.js +++ b/public/app/features/panel/panelSrv.js @@ -70,6 +70,14 @@ function (angular, _, config) { }; $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 }); }; diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index 73bcfb662bd..10ff556bf84 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -29,13 +29,7 @@ function (angular, _, kbn) { var variable = this.variables[i]; var urlValue = queryParams['var-' + variable.name]; 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) { promises.push(this.updateOptions(variable)); @@ -48,11 +42,30 @@ function (angular, _, kbn) { 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) { if (!variable.auto) { return; } // 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' }); } diff --git a/public/app/panels/graph/axisEditor.html b/public/app/panels/graph/axisEditor.html index 1d8e459be3b..ae69ec60929 100644 --- a/public/app/panels/graph/axisEditor.html +++ b/public/app/panels/graph/axisEditor.html @@ -228,10 +228,10 @@
      -
    • +
    • Decimals
    • -
    • +
    • @@ -242,4 +242,3 @@
    - diff --git a/public/app/panels/graph/graph.js b/public/app/panels/graph/graph.js index a26aa7a656e..795aee03c20 100755 --- a/public/app/panels/graph/graph.js +++ b/public/app/panels/graph/graph.js @@ -63,12 +63,13 @@ function (angular, $, kbn, moment, _, GraphTooltip) { render_panel(); }); - function getLegendHeight() { + function getLegendHeight(panelHeight) { if (!scope.panel.legend.show || scope.panel.legend.rightSide) { return 0; } 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 { return 26; } @@ -84,7 +85,7 @@ function (angular, $, kbn, moment, _, GraphTooltip) { graphHeight -= 5; // padding 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'); diff --git a/public/app/panels/graph/module.js b/public/app/panels/graph/module.js index 4fd56fb69b4..dfcd5901594 100644 --- a/public/app/panels/graph/module.js +++ b/public/app/panels/graph/module.js @@ -29,7 +29,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { panelName: 'Graph', editIcon: "fa fa-bar-chart", fullscreen: true, - metricsEditor: true + metricsEditor: true, }); $scope.panelMeta.addEditorTab('Axes & Grid', 'app/panels/graph/axisEditor.html'); @@ -67,9 +67,9 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { // show/hide lines lines : true, // fill factor - fill : 0, + fill : 1, // line width in pixels - linewidth : 1, + linewidth : 2, // show hide points points : false, // point radius in pixels diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html index 1384c41c1ee..1a4bd402879 100644 --- a/public/app/partials/dashboard.html +++ b/public/app/partials/dashboard.html @@ -24,7 +24,7 @@
    - -
    +
    ADD ROW diff --git a/public/app/partials/dasheditor.html b/public/app/partials/dasheditor.html index e6ab6070f9f..fefded81efc 100644 --- a/public/app/partials/dasheditor.html +++ b/public/app/partials/dasheditor.html @@ -17,63 +17,118 @@
    -
    - -
    -
    -
    -
    - -
    -
    - - -
    - - +
    +
    +
    +
    +
    Dashboard info
    +
    +
      +
    • + Title +
    • +
    • + +
    • +
    • + Tags + Press enter to a add tag +
    • +
    • + + +
    • +
    +
    +
    +
      +
    • + Timezone +
    • +
    • + +
    • +
    +
    +
    +
    -
    -
    -
    - - - - Press enter to a add tag -
    + +
    +
    Toggles
    +
    +
      +
    • + +
    • +
    • +
    • + + +
    • + +
    +
    +
    +
    +
      +
    • + +
    • +
    • + + +
    • +
    +
    +
    +
    +
      +
    • + +
    • +
    • + + +
    • +
    +
    - -
    -
    -
    - - - - - - - -
    - {{row.title}} - - - - -
    -
    -
    -
    - -
    - - -
    -
    -
    +
    +
    +
    + + + + + + + +
    + {{row.title}} + + + + +
    +
    +
    +
    +
    + +
    + + +
    + +
    + +
    - diff --git a/public/app/routes/dashLoadControllers.js b/public/app/routes/dashLoadControllers.js index b19b0c9664f..ee36ef72117 100644 --- a/public/app/routes/dashLoadControllers.js +++ b/public/app/routes/dashLoadControllers.js @@ -18,6 +18,8 @@ function (angular, _, kbn, moment, $) { if (!$routeParams.slug) { 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); },function() { dashboardLoadFailed('Not found'); @@ -38,7 +40,16 @@ function (angular, _, kbn, moment, $) { backendSrv.get('/api/snapshots/' + $routeParams.key).then(function(result) { $scope.initDashboard(result, $scope); }, 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(''); return; } - $scope.initDashboard({meta: {}, model: window.grafanaImportDashboard }, $scope); + $scope.initDashboard({ + meta: { canShare: false, canStar: false }, + model: window.grafanaImportDashboard + }, $scope); }); module.controller('NewDashboardCtrl', function($scope) { $scope.initDashboard({ - meta: {}, + meta: { canStar: false, canShare: false }, model: { title: "New dashboard", - rows: [{ height: '250px', panels:[] }] + rows: [{ height: '250px', panels:[] }] }, }, $scope); }); @@ -66,10 +80,10 @@ function (angular, _, kbn, moment, $) { var file_load = function(file) { return $http({ url: "public/dashboards/"+file.replace(/\.(?!json)/,"/")+'?' + new Date().getTime(), - method: "GET", - transformResponse: function(response) { - return angular.fromJson(response); - } + method: "GET", + transformResponse: function(response) { + return angular.fromJson(response); + } }).then(function(result) { if(!result) { return false; @@ -82,7 +96,10 @@ function (angular, _, kbn, moment, $) { }; 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 services = { dashboardSrv: dashboardSrv, - datasourceSrv: datasourceSrv, - $q: $q, + datasourceSrv: datasourceSrv, + $q: $q, }; /*jshint -W054 */ @@ -118,16 +135,19 @@ function (angular, _, kbn, moment, $) { var url = 'public/dashboards/'+file.replace(/\.(?!js)/,"/") + '?' + new Date().getTime(); 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) { - $scope.initDashboard({meta: {fromScript: true}, model: result.data}, $scope); + $scope.initDashboard({ + meta: {fromScript: true, canDelete: false, canSave: false}, + model: result.data + }, $scope); }); }); diff --git a/public/app/services/backendSrv.js b/public/app/services/backendSrv.js index 30cad361bbe..c208fe3388e 100644 --- a/public/app/services/backendSrv.js +++ b/public/app/services/backendSrv.js @@ -63,8 +63,9 @@ function (angular, _, config) { var requestIsLocal = options.url.indexOf('/') === 0; var firstAttempt = options.retry === 0; - if (requestIsLocal && firstAttempt) { + if (requestIsLocal && !options.hasSubUrl) { options.url = config.appSubUrl + options.url; + options.hasSubUrl = true; } return $http(options).then(function(results) { diff --git a/public/app/services/contextSrv.js b/public/app/services/contextSrv.js index c615e0baf8c..99bbcccf156 100644 --- a/public/app/services/contextSrv.js +++ b/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 $rootScope.$on('toggle-sidemenu', function() { self.toggleSideMenu(); @@ -47,6 +40,12 @@ function (angular, _, store, config) { }, 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'); }); - }); diff --git a/public/css/less/bootstrap-tagsinput.less b/public/css/less/bootstrap-tagsinput.less index 42466919afe..aeedb317997 100644 --- a/public/css/less/bootstrap-tagsinput.less +++ b/public/css/less/bootstrap-tagsinput.less @@ -1,33 +1,19 @@ .bootstrap-tagsinput { display: inline-block; - padding: 4px 6px; - margin-bottom: 10px; - color: #555; + padding: 0 0 0 6px; vertical-align: middle; - border-radius: 4px; max-width: 100%; line-height: 22px; - 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 { 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 { @@ -49,4 +35,4 @@ } } } -} \ No newline at end of file +} diff --git a/public/css/less/search.less b/public/css/less/search.less index 9b3da69f0e2..7182333bd16 100644 --- a/public/css/less/search.less +++ b/public/css/less/search.less @@ -22,7 +22,8 @@ padding-bottom: 10px; input { width: 100%; - padding: 18px 8px; + padding: 8px 8px; + height: 100%; box-sizing: border-box; } button { diff --git a/public/test/specs/dashboardSrv-specs.js b/public/test/specs/dashboardSrv-specs.js index 19f81f1cc01..35d248888c7 100644 --- a/public/test/specs/dashboardSrv-specs.js +++ b/public/test/specs/dashboardSrv-specs.js @@ -185,10 +185,26 @@ define([ expect(model.annotations.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); + }); + }); }); - - }); diff --git a/public/vendor/tagsinput/bootstrap-tagsinput.js b/public/vendor/tagsinput/bootstrap-tagsinput.js index be94965d807..5c8e25a5761 100644 --- a/public/vendor/tagsinput/bootstrap-tagsinput.js +++ b/public/vendor/tagsinput/bootstrap-tagsinput.js @@ -500,4 +500,4 @@ $(function() { $("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput(); }); -})(window.jQuery); \ No newline at end of file +})(window.jQuery); diff --git a/tasks/options/compress.js b/tasks/options/compress.js index 28ee6318fcf..4dc77ec82f8 100644 --- a/tasks/options/compress.js +++ b/tasks/options/compress.js @@ -16,7 +16,7 @@ module.exports = function(config) { { expand: true, src: ['LICENSE.md', 'README.md', 'NOTICE.md'], - dest: '<%= pkg.name %>/', + dest: '<%= pkg.name %>-<%= pkg.version %>/', } ] }