Przeglądaj źródła

Merge branch 'template_var_multi_select' into panel_repeat

Conflicts:
	src/app/features/dashboard/submenuCtrl.js
	src/test/specs/templateSrv-specs.js
Torkel Ödegaard 10 lat temu
rodzic
commit
c7b4041879
53 zmienionych plików z 986 dodań i 376 usunięć
  1. 1 0
      CHANGELOG.md
  2. 3 3
      Godeps/Godeps.json
  3. 7 4
      Godeps/_workspace/src/github.com/Unknwon/macaron/README.md
  4. 0 37
      Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/README.md
  5. 0 45
      Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/bufferpool.go
  6. 69 12
      Godeps/_workspace/src/github.com/Unknwon/macaron/context.go
  7. 81 25
      Godeps/_workspace/src/github.com/Unknwon/macaron/context_test.go
  8. 1 1
      Godeps/_workspace/src/github.com/Unknwon/macaron/inject/inject_test.go
  9. 1 1
      Godeps/_workspace/src/github.com/Unknwon/macaron/logger_test.go
  10. 2 2
      Godeps/_workspace/src/github.com/Unknwon/macaron/macaron.go
  11. 37 5
      Godeps/_workspace/src/github.com/Unknwon/macaron/render.go
  12. 17 17
      Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler.go
  13. 1 1
      Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler_test.go
  14. 3 1
      Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore
  15. 34 2
      Godeps/_workspace/src/gopkg.in/ini.v1/README.md
  16. 32 2
      Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md
  17. 99 7
      Godeps/_workspace/src/gopkg.in/ini.v1/ini.go
  18. 45 6
      Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go
  19. 12 6
      Godeps/_workspace/src/gopkg.in/ini.v1/struct.go
  20. 21 23
      Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go
  21. 2 2
      docs/sources/guides/changes_in_v2.md
  22. 4 0
      pkg/cmd/web.go
  23. 8 0
      pkg/models/dashboards.go
  24. 1 1
      pkg/services/sqlstore/migrations/apikey_mig.go
  25. 6 0
      pkg/services/sqlstore/user.go
  26. 0 19
      src/app/controllers/search.js
  27. 1 0
      src/app/directives/all.js
  28. 26 0
      src/app/directives/giveFocus.js
  29. 110 0
      src/app/directives/templateParamSelector.js
  30. 3 2
      src/app/directives/tip.js
  31. 3 3
      src/app/features/dashboard/partials/shareModal.html
  32. 24 0
      src/app/features/dashboard/partials/variableValueSelect.html
  33. 8 10
      src/app/features/dashboard/sharePanelCtrl.js
  34. 2 2
      src/app/features/dashboard/submenuCtrl.js
  35. 2 2
      src/app/features/panel/panelDirective.js
  36. 3 1
      src/app/features/templating/editorCtrl.js
  37. 13 1
      src/app/features/templating/templateSrv.js
  38. 5 0
      src/app/features/templating/templateValuesSrv.js
  39. 12 0
      src/app/panels/graph/axisEditor.html
  40. 89 42
      src/app/panels/graph/graph.js
  41. 1 1
      src/app/panels/graph/graph.tooltip.js
  42. 5 1
      src/app/panels/graph/module.js
  43. 0 1
      src/app/partials/dasheditor.html
  44. 1 1
      src/app/partials/search.html
  45. 17 8
      src/app/partials/submenu.html
  46. 92 70
      src/app/partials/templating_editor.html
  47. 5 0
      src/css/less/forms.less
  48. 1 0
      src/css/less/grafana.less
  49. 44 1
      src/css/less/submenu.less
  50. 0 4
      src/css/less/tightform.less
  51. 1 1
      src/css/less/variables.dark.less
  52. 3 3
      src/test/specs/sharePanelCtrl-specs.js
  53. 28 0
      src/test/specs/templateSrv-specs.js

+ 1 - 0
CHANGELOG.md

@@ -7,6 +7,7 @@
 - [Issue #171](https://github.com/grafana/grafana/issues/171).   Panel: Different time periods, panels can override dashboard relative time and/or add a time shift
 - [Issue #171](https://github.com/grafana/grafana/issues/171).   Panel: Different time periods, panels can override dashboard relative time and/or add a time shift
 - [Issue #1488](https://github.com/grafana/grafana/issues/1488). Dashboard: Clone dashboard / Save as
 - [Issue #1488](https://github.com/grafana/grafana/issues/1488). Dashboard: Clone dashboard / Save as
 - [Issue #1458](https://github.com/grafana/grafana/issues/1458). User: persisted user option for dark or light theme  (no longer an option on a dashboard)
 - [Issue #1458](https://github.com/grafana/grafana/issues/1458). User: persisted user option for dark or light theme  (no longer an option on a dashboard)
+- [Issue #452](https://github.com/grafana/grafana/issues/452).   Graph: Adds logarithmic scale option (log base 10)
 
 
 **Enhancements**
 **Enhancements**
 - [Issue #1366](https://github.com/grafana/grafana/issues/1366). Graph & Singlestat: Support for additional units, Fahrenheit (°F) and Celsius (°C), Humidity (%H), kW, watt-hour (Wh), kilowatt-hour (kWh), velocities (m/s, km/h, mpg, knot)
 - [Issue #1366](https://github.com/grafana/grafana/issues/1366). Graph & Singlestat: Support for additional units, Fahrenheit (°F) and Celsius (°C), Humidity (%H), kW, watt-hour (Wh), kilowatt-hour (kWh), velocities (m/s, km/h, mpg, knot)

+ 3 - 3
Godeps/Godeps.json

@@ -11,7 +11,7 @@
 		},
 		},
 		{
 		{
 			"ImportPath": "github.com/Unknwon/macaron",
 			"ImportPath": "github.com/Unknwon/macaron",
-			"Rev": "da7cbddc50b9d33e076fb1eabff13b55c3b85fc5"
+			"Rev": "93de4f3fad97bf246b838f828e2348f46f21f20a"
 		},
 		},
 		{
 		{
 			"ImportPath": "github.com/codegangsta/cli",
 			"ImportPath": "github.com/codegangsta/cli",
@@ -72,8 +72,8 @@
 		},
 		},
 		{
 		{
 			"ImportPath": "gopkg.in/ini.v1",
 			"ImportPath": "gopkg.in/ini.v1",
-			"Comment": "v0-10-g28ad8c4",
-			"Rev": "28ad8c408ba20e5c86b06d64cd2cc9248f640a83"
+			"Comment": "v0-16-g1772191",
+			"Rev": "177219109c97e7920c933e21c9b25f874357b237"
 		}
 		}
 	]
 	]
 }
 }

+ 7 - 4
Godeps/_workspace/src/github.com/Unknwon/macaron/README.md

@@ -1,11 +1,11 @@
 Macaron [![Build Status](https://drone.io/github.com/Unknwon/macaron/status.png)](https://drone.io/github.com/Unknwon/macaron/latest) [![](http://gocover.io/_badge/github.com/Unknwon/macaron)](http://gocover.io/github.com/Unknwon/macaron)
 Macaron [![Build Status](https://drone.io/github.com/Unknwon/macaron/status.png)](https://drone.io/github.com/Unknwon/macaron/latest) [![](http://gocover.io/_badge/github.com/Unknwon/macaron)](http://gocover.io/github.com/Unknwon/macaron)
 =======================
 =======================
 
 
-![Macaron Logo](macaronlogo.png)
+![Macaron Logo](https://raw.githubusercontent.com/Unknwon/macaron/master/macaronlogo.png)
 
 
 Package macaron is a high productive and modular design web framework in Go.
 Package macaron is a high productive and modular design web framework in Go.
 
 
-##### Current version: 0.5.0
+##### Current version: 0.5.4
 
 
 ## Getting Started
 ## Getting Started
 
 
@@ -41,7 +41,7 @@ func main() {
 - Handy dependency injection powered by [inject](https://github.com/codegangsta/inject).
 - Handy dependency injection powered by [inject](https://github.com/codegangsta/inject).
 - Better router layer and less reflection make faster speed.
 - Better router layer and less reflection make faster speed.
 
 
-## Middlewares 
+## Middlewares
 
 
 Middlewares allow you easily plugin/unplugin features for your Macaron applications.
 Middlewares allow you easily plugin/unplugin features for your Macaron applications.
 
 
@@ -70,15 +70,18 @@ There are already many [middlewares](https://github.com/macaron-contrib) to simp
 
 
 - [Gogs](https://github.com/gogits/gogs): Go Git Service
 - [Gogs](https://github.com/gogits/gogs): Go Git Service
 - [Gogs Web](https://github.com/gogits/gogsweb): Gogs official website
 - [Gogs Web](https://github.com/gogits/gogsweb): Gogs official website
+- [Go Walker](https://gowalker.org): Go online API documentation
 - [Switch](https://github.com/gpmgo/switch): Gopm registry
 - [Switch](https://github.com/gpmgo/switch): Gopm registry
 - [YouGam](http://yougam.com): Online Forum
 - [YouGam](http://yougam.com): Online Forum
 - [Car Girl](http://qcnl.gzsy.com/): Online campaign
 - [Car Girl](http://qcnl.gzsy.com/): Online campaign
+- [Critical Stack Intel](https://intel.criticalstack.com/): A 100% free intel marketplace from Critical Stack, Inc.
 
 
 ## Getting Help
 ## Getting Help
 
 
 - [API Reference](https://gowalker.org/github.com/Unknwon/macaron)
 - [API Reference](https://gowalker.org/github.com/Unknwon/macaron)
 - [Documentation](http://macaron.gogs.io)
 - [Documentation](http://macaron.gogs.io)
 - [FAQs](http://macaron.gogs.io/docs/faqs)
 - [FAQs](http://macaron.gogs.io/docs/faqs)
+- [![Join the chat at https://gitter.im/Unknwon/macaron](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Unknwon/macaron?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 
 
 ## Credits
 ## Credits
 
 
@@ -88,4 +91,4 @@ There are already many [middlewares](https://github.com/macaron-contrib) to simp
 
 
 ## License
 ## License
 
 
-This project is under Apache v2 License. See the [LICENSE](LICENSE) file for the full license text.
+This project is under Apache v2 License. See the [LICENSE](LICENSE) file for the full license text.

+ 0 - 37
Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/README.md

@@ -1,37 +0,0 @@
-# bpool [![GoDoc](https://godoc.org/github.com/oxtoacart/bpool?status.png)](https://godoc.org/github.com/oxtoacart/bpool)
-
-Package bpool implements leaky pools of byte arrays and Buffers as bounded channels. It is based on the leaky buffer example from the Effective Go documentation: http://golang.org/doc/effective_go.html#leaky_buffer
-
-## Install
-
-`go get github.com/oxtoacart/bpool`
-
-## Documentation
-
-See [godoc.org](http://godoc.org/github.com/oxtoacart/bpool) or use `godoc github.com/oxtoacart/bpool`
-
-## Example
-
-```go
-
-var bufpool *bpol.BufferPool
-
-func main() {
-
-    bufpool = bpool.NewBufferPool(48)
-
-}
-
-func someFunction() error {
-
-     // Get a buffer from the pool
-     buf := bufpool.Get()
-     ...
-     ...
-     ...
-     // Return the buffer to the pool
-     bufpool.Put(buf)
-     
-     return nil
-}
-```

+ 0 - 45
Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/bufferpool.go

@@ -1,45 +0,0 @@
-package bpool
-
-import (
-	"bytes"
-)
-
-/*
-BufferPool implements a pool of bytes.Buffers in the form of a bounded
-channel.
-*/
-type BufferPool struct {
-	c chan *bytes.Buffer
-}
-
-/*
-NewBufferPool creates a new BufferPool bounded to the given size.
-*/
-func NewBufferPool(size int) (bp *BufferPool) {
-	return &BufferPool{
-		c: make(chan *bytes.Buffer, size),
-	}
-}
-
-/*
-Get gets a Buffer from the BufferPool, or creates a new one if none are available
-in the pool.
-*/
-func (bp *BufferPool) Get() (b *bytes.Buffer) {
-	select {
-	case b = <-bp.c:
-	// reuse existing buffer
-	default:
-		// create new buffer
-		b = bytes.NewBuffer([]byte{})
-	}
-	return
-}
-
-/*
-Put returns the given Buffer to the BufferPool.
-*/
-func (bp *BufferPool) Put(b *bytes.Buffer) {
-	b.Reset()
-	bp.c <- b
-}

+ 69 - 12
Godeps/_workspace/src/github.com/Unknwon/macaron/context.go

@@ -23,9 +23,11 @@ import (
 	"mime/multipart"
 	"mime/multipart"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
+	"os"
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
 	"reflect"
 	"reflect"
+	"strconv"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -182,6 +184,11 @@ func (ctx *Context) Query(name string) string {
 	return ctx.Req.Form.Get(name)
 	return ctx.Req.Form.Get(name)
 }
 }
 
 
+// QueryTrim querys and trims spaces form parameter.
+func (ctx *Context) QueryTrim(name string) string {
+	return strings.TrimSpace(ctx.Query(name))
+}
+
 // QueryStrings returns a list of results by given query name.
 // QueryStrings returns a list of results by given query name.
 func (ctx *Context) QueryStrings(name string) []string {
 func (ctx *Context) QueryStrings(name string) []string {
 	if ctx.Req.Form == nil {
 	if ctx.Req.Form == nil {
@@ -210,9 +217,21 @@ func (ctx *Context) QueryInt64(name string) int64 {
 	return com.StrTo(ctx.Query(name)).MustInt64()
 	return com.StrTo(ctx.Query(name)).MustInt64()
 }
 }
 
 
+// QueryFloat64 returns query result in float64 type.
+func (ctx *Context) QueryFloat64(name string) float64 {
+	v, _ := strconv.ParseFloat(ctx.Query(name), 64)
+	return v
+}
+
 // Params returns value of given param name.
 // Params returns value of given param name.
-// e.g. ctx.Params(":uid")
+// e.g. ctx.Params(":uid") or ctx.Params("uid")
 func (ctx *Context) Params(name string) string {
 func (ctx *Context) Params(name string) string {
+	if len(name) == 0 {
+		return ""
+	}
+	if name[0] != '*' && name[0] != ':' {
+		name = ":" + name
+	}
 	return ctx.params[name]
 	return ctx.params[name]
 }
 }
 
 
@@ -242,12 +261,20 @@ func (ctx *Context) ParamsInt64(name string) int64 {
 	return com.StrTo(ctx.Params(name)).MustInt64()
 	return com.StrTo(ctx.Params(name)).MustInt64()
 }
 }
 
 
+// ParamsFloat64 returns params result in int64 type.
+// e.g. ctx.ParamsFloat64(":uid")
+func (ctx *Context) ParamsFloat64(name string) float64 {
+	v, _ := strconv.ParseFloat(ctx.Params(name), 64)
+	return v
+}
+
 // GetFile returns information about user upload file by given form field name.
 // GetFile returns information about user upload file by given form field name.
 func (ctx *Context) GetFile(name string) (multipart.File, *multipart.FileHeader, error) {
 func (ctx *Context) GetFile(name string) (multipart.File, *multipart.FileHeader, error) {
 	return ctx.Req.FormFile(name)
 	return ctx.Req.FormFile(name)
 }
 }
 
 
 // SetCookie sets given cookie value to response header.
 // SetCookie sets given cookie value to response header.
+// FIXME: IE support? http://golanghome.com/post/620#reply2
 func (ctx *Context) SetCookie(name string, value string, others ...interface{}) {
 func (ctx *Context) SetCookie(name string, value string, others ...interface{}) {
 	cookie := http.Cookie{}
 	cookie := http.Cookie{}
 	cookie.Name = name
 	cookie.Name = name
@@ -264,23 +291,19 @@ func (ctx *Context) SetCookie(name string, value string, others ...interface{})
 		}
 		}
 	}
 	}
 
 
-	// default "/"
+	cookie.Path = "/"
 	if len(others) > 1 {
 	if len(others) > 1 {
 		if v, ok := others[1].(string); ok && len(v) > 0 {
 		if v, ok := others[1].(string); ok && len(v) > 0 {
 			cookie.Path = v
 			cookie.Path = v
 		}
 		}
-	} else {
-		cookie.Path = "/"
 	}
 	}
 
 
-	// default empty
 	if len(others) > 2 {
 	if len(others) > 2 {
 		if v, ok := others[2].(string); ok && len(v) > 0 {
 		if v, ok := others[2].(string); ok && len(v) > 0 {
 			cookie.Domain = v
 			cookie.Domain = v
 		}
 		}
 	}
 	}
 
 
-	// default empty
 	if len(others) > 3 {
 	if len(others) > 3 {
 		switch v := others[3].(type) {
 		switch v := others[3].(type) {
 		case bool:
 		case bool:
@@ -292,7 +315,6 @@ func (ctx *Context) SetCookie(name string, value string, others ...interface{})
 		}
 		}
 	}
 	}
 
 
-	// default false. for session cookie default true
 	if len(others) > 4 {
 	if len(others) > 4 {
 		if v, ok := others[4].(bool); ok && v {
 		if v, ok := others[4].(bool); ok && v {
 			cookie.HttpOnly = true
 			cookie.HttpOnly = true
@@ -322,6 +344,12 @@ func (ctx *Context) GetCookieInt64(name string) int64 {
 	return com.StrTo(ctx.GetCookie(name)).MustInt64()
 	return com.StrTo(ctx.GetCookie(name)).MustInt64()
 }
 }
 
 
+// GetCookieFloat64 returns cookie result in float64 type.
+func (ctx *Context) GetCookieFloat64(name string) float64 {
+	v, _ := strconv.ParseFloat(ctx.GetCookie(name), 64)
+	return v
+}
+
 var defaultCookieSecret string
 var defaultCookieSecret string
 
 
 // SetDefaultCookieSecret sets global default secure cookie secret.
 // SetDefaultCookieSecret sets global default secure cookie secret.
@@ -368,6 +396,14 @@ func (ctx *Context) GetSuperSecureCookie(secret, key string) (string, bool) {
 	return string(text), err == nil
 	return string(text), err == nil
 }
 }
 
 
+func (ctx *Context) setRawContentHeader() {
+	ctx.Resp.Header().Set("Content-Description", "Raw content")
+	ctx.Resp.Header().Set("Content-Type", "text/plain")
+	ctx.Resp.Header().Set("Expires", "0")
+	ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
+	ctx.Resp.Header().Set("Pragma", "public")
+}
+
 // ServeContent serves given content to response.
 // ServeContent serves given content to response.
 func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interface{}) {
 func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interface{}) {
 	modtime := time.Now()
 	modtime := time.Now()
@@ -377,14 +413,35 @@ func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interfa
 			modtime = v
 			modtime = v
 		}
 		}
 	}
 	}
-	ctx.Resp.Header().Set("Content-Description", "Raw content")
-	ctx.Resp.Header().Set("Content-Type", "text/plain")
-	ctx.Resp.Header().Set("Expires", "0")
-	ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
-	ctx.Resp.Header().Set("Pragma", "public")
+
+	ctx.setRawContentHeader()
 	http.ServeContent(ctx.Resp, ctx.Req.Request, name, modtime, r)
 	http.ServeContent(ctx.Resp, ctx.Req.Request, name, modtime, r)
 }
 }
 
 
+// ServeFileContent serves given file as content to response.
+func (ctx *Context) ServeFileContent(file string, names ...string) {
+	var name string
+	if len(names) > 0 {
+		name = names[0]
+	} else {
+		name = path.Base(file)
+	}
+
+	f, err := os.Open(file)
+	if err != nil {
+		if Env == PROD {
+			http.Error(ctx.Resp, "Internal Server Error", 500)
+		} else {
+			http.Error(ctx.Resp, err.Error(), 500)
+		}
+		return
+	}
+	defer f.Close()
+
+	ctx.setRawContentHeader()
+	http.ServeContent(ctx.Resp, ctx.Req.Request, name, time.Now(), f)
+}
+
 // ServeFile serves given file to response.
 // ServeFile serves given file to response.
 func (ctx *Context) ServeFile(file string, names ...string) {
 func (ctx *Context) ServeFile(file string, names ...string) {
 	var name string
 	var name string

+ 81 - 25
Godeps/_workspace/src/github.com/Unknwon/macaron/context_test.go

@@ -76,35 +76,53 @@ func Test_Context(t *testing.T) {
 		})
 		})
 
 
 		Convey("Render HTML", func() {
 		Convey("Render HTML", func() {
-			m.Get("/html", func(ctx *Context) {
-				ctx.HTML(304, "hello", "Unknwon") // 304 for logger test.
+
+			Convey("Normal HTML", func() {
+				m.Get("/html", func(ctx *Context) {
+					ctx.HTML(304, "hello", "Unknwon") // 304 for logger test.
+				})
+
+				resp := httptest.NewRecorder()
+				req, err := http.NewRequest("GET", "/html", nil)
+				So(err, ShouldBeNil)
+				m.ServeHTTP(resp, req)
+				So(resp.Body.String(), ShouldEqual, "<h1>Hello Unknwon</h1>")
 			})
 			})
 
 
-			resp := httptest.NewRecorder()
-			req, err := http.NewRequest("GET", "/html", nil)
-			So(err, ShouldBeNil)
-			m.ServeHTTP(resp, req)
-			So(resp.Body.String(), ShouldEqual, "<h1>Hello Unknwon</h1>")
+			Convey("HTML template set", func() {
+				m.Get("/html2", func(ctx *Context) {
+					ctx.Data["Name"] = "Unknwon"
+					ctx.HTMLSet(200, "basic2", "hello2")
+				})
 
 
-			m.Get("/html2", func(ctx *Context) {
-				ctx.Data["Name"] = "Unknwon"
-				ctx.HTMLSet(200, "basic2", "hello2")
+				resp := httptest.NewRecorder()
+				req, err := http.NewRequest("GET", "/html2", nil)
+				So(err, ShouldBeNil)
+				m.ServeHTTP(resp, req)
+				So(resp.Body.String(), ShouldEqual, "<h1>Hello Unknwon</h1>")
 			})
 			})
 
 
-			resp = httptest.NewRecorder()
-			req, err = http.NewRequest("GET", "/html2", nil)
-			So(err, ShouldBeNil)
-			m.ServeHTTP(resp, req)
-			So(resp.Body.String(), ShouldEqual, "<h1>Hello Unknwon</h1>")
+			Convey("With layout", func() {
+				m.Get("/layout", func(ctx *Context) {
+					ctx.HTML(200, "hello", "Unknwon", HTMLOptions{"layout"})
+				})
+
+				resp := httptest.NewRecorder()
+				req, err := http.NewRequest("GET", "/layout", nil)
+				So(err, ShouldBeNil)
+				m.ServeHTTP(resp, req)
+				So(resp.Body.String(), ShouldEqual, "head<h1>Hello Unknwon</h1>foot")
+			})
 		})
 		})
 
 
 		Convey("Parse from and query", func() {
 		Convey("Parse from and query", func() {
 			m.Get("/query", func(ctx *Context) string {
 			m.Get("/query", func(ctx *Context) string {
 				var buf bytes.Buffer
 				var buf bytes.Buffer
-				buf.WriteString(ctx.Query("name") + " ")
+				buf.WriteString(ctx.QueryTrim("name") + " ")
 				buf.WriteString(ctx.QueryEscape("name") + " ")
 				buf.WriteString(ctx.QueryEscape("name") + " ")
 				buf.WriteString(com.ToStr(ctx.QueryInt("int")) + " ")
 				buf.WriteString(com.ToStr(ctx.QueryInt("int")) + " ")
 				buf.WriteString(com.ToStr(ctx.QueryInt64("int64")) + " ")
 				buf.WriteString(com.ToStr(ctx.QueryInt64("int64")) + " ")
+				buf.WriteString(com.ToStr(ctx.QueryFloat64("float64")) + " ")
 				return buf.String()
 				return buf.String()
 			})
 			})
 			m.Get("/query2", func(ctx *Context) string {
 			m.Get("/query2", func(ctx *Context) string {
@@ -115,10 +133,10 @@ func Test_Context(t *testing.T) {
 			})
 			})
 
 
 			resp := httptest.NewRecorder()
 			resp := httptest.NewRecorder()
-			req, err := http.NewRequest("GET", "/query?name=Unknwon&int=12&int64=123", nil)
+			req, err := http.NewRequest("GET", "/query?name=Unknwon&int=12&int64=123&float64=1.25", nil)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 			m.ServeHTTP(resp, req)
 			m.ServeHTTP(resp, req)
-			So(resp.Body.String(), ShouldEqual, "Unknwon Unknwon 12 123 ")
+			So(resp.Body.String(), ShouldEqual, "Unknwon Unknwon 12 123 1.25 ")
 
 
 			resp = httptest.NewRecorder()
 			resp = httptest.NewRecorder()
 			req, err = http.NewRequest("GET", "/query2?list=item1&list=item2", nil)
 			req, err = http.NewRequest("GET", "/query2?list=item1&list=item2", nil)
@@ -128,21 +146,23 @@ func Test_Context(t *testing.T) {
 		})
 		})
 
 
 		Convey("URL parameter", func() {
 		Convey("URL parameter", func() {
-			m.Get("/:name/:int/:int64", func(ctx *Context) string {
+			m.Get("/:name/:int/:int64/:float64", func(ctx *Context) string {
 				var buf bytes.Buffer
 				var buf bytes.Buffer
-				ctx.SetParams(":name", ctx.Params(":name"))
+				ctx.SetParams("name", ctx.Params("name"))
+				buf.WriteString(ctx.Params(""))
 				buf.WriteString(ctx.Params(":name") + " ")
 				buf.WriteString(ctx.Params(":name") + " ")
 				buf.WriteString(ctx.ParamsEscape(":name") + " ")
 				buf.WriteString(ctx.ParamsEscape(":name") + " ")
 				buf.WriteString(com.ToStr(ctx.ParamsInt(":int")) + " ")
 				buf.WriteString(com.ToStr(ctx.ParamsInt(":int")) + " ")
 				buf.WriteString(com.ToStr(ctx.ParamsInt64(":int64")) + " ")
 				buf.WriteString(com.ToStr(ctx.ParamsInt64(":int64")) + " ")
+				buf.WriteString(com.ToStr(ctx.ParamsFloat64(":float64")) + " ")
 				return buf.String()
 				return buf.String()
 			})
 			})
 
 
 			resp := httptest.NewRecorder()
 			resp := httptest.NewRecorder()
-			req, err := http.NewRequest("GET", "/user/1/13", nil)
+			req, err := http.NewRequest("GET", "/user/1/13/1.24", nil)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 			m.ServeHTTP(resp, req)
 			m.ServeHTTP(resp, req)
-			So(resp.Body.String(), ShouldEqual, "user user 1 13 ")
+			So(resp.Body.String(), ShouldEqual, "user user 1 13 1.24 ")
 		})
 		})
 
 
 		Convey("Get file", func() {
 		Convey("Get file", func() {
@@ -158,26 +178,29 @@ func Test_Context(t *testing.T) {
 
 
 		Convey("Set and get cookie", func() {
 		Convey("Set and get cookie", func() {
 			m.Get("/set", func(ctx *Context) {
 			m.Get("/set", func(ctx *Context) {
-				ctx.SetCookie("user", "Unknwon", 1)
+				ctx.SetCookie("user", "Unknwon", 1, "/", "localhost", true, true)
+				ctx.SetCookie("user", "Unknwon", int32(1), "/", "localhost", 1)
+				ctx.SetCookie("user", "Unknwon", int64(1))
 			})
 			})
 
 
 			resp := httptest.NewRecorder()
 			resp := httptest.NewRecorder()
 			req, err := http.NewRequest("GET", "/set", nil)
 			req, err := http.NewRequest("GET", "/set", nil)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 			m.ServeHTTP(resp, req)
 			m.ServeHTTP(resp, req)
-			So(resp.Header().Get("Set-Cookie"), ShouldEqual, "user=Unknwon; Path=/; Max-Age=1")
+			So(resp.Header().Get("Set-Cookie"), ShouldEqual, "user=Unknwon; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure")
 
 
 			m.Get("/get", func(ctx *Context) string {
 			m.Get("/get", func(ctx *Context) string {
 				ctx.GetCookie("404")
 				ctx.GetCookie("404")
 				So(ctx.GetCookieInt("uid"), ShouldEqual, 1)
 				So(ctx.GetCookieInt("uid"), ShouldEqual, 1)
 				So(ctx.GetCookieInt64("uid"), ShouldEqual, 1)
 				So(ctx.GetCookieInt64("uid"), ShouldEqual, 1)
+				So(ctx.GetCookieFloat64("balance"), ShouldEqual, 1.25)
 				return ctx.GetCookie("user")
 				return ctx.GetCookie("user")
 			})
 			})
 
 
 			resp = httptest.NewRecorder()
 			resp = httptest.NewRecorder()
 			req, err = http.NewRequest("GET", "/get", nil)
 			req, err = http.NewRequest("GET", "/get", nil)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
-			req.Header.Set("Cookie", "user=Unknwon; uid=1")
+			req.Header.Set("Cookie", "user=Unknwon; uid=1; balance=1.25")
 			m.ServeHTTP(resp, req)
 			m.ServeHTTP(resp, req)
 			So(resp.Body.String(), ShouldEqual, "Unknwon")
 			So(resp.Body.String(), ShouldEqual, "Unknwon")
 		})
 		})
@@ -231,6 +254,39 @@ func Test_Context(t *testing.T) {
 			So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
 			So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
 		})
 		})
 
 
+		Convey("Serve file content", func() {
+			m.Get("/file", func(ctx *Context) {
+				ctx.ServeFileContent("fixtures/custom_funcs/index.tmpl")
+			})
+
+			resp := httptest.NewRecorder()
+			req, err := http.NewRequest("GET", "/file", nil)
+			So(err, ShouldBeNil)
+			m.ServeHTTP(resp, req)
+			So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
+
+			m.Get("/file2", func(ctx *Context) {
+				ctx.ServeFileContent("fixtures/custom_funcs/index.tmpl", "ok.tmpl")
+			})
+
+			resp = httptest.NewRecorder()
+			req, err = http.NewRequest("GET", "/file2", nil)
+			So(err, ShouldBeNil)
+			m.ServeHTTP(resp, req)
+			So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
+
+			m.Get("/file3", func(ctx *Context) {
+				ctx.ServeFileContent("404.tmpl")
+			})
+
+			resp = httptest.NewRecorder()
+			req, err = http.NewRequest("GET", "/file3", nil)
+			So(err, ShouldBeNil)
+			m.ServeHTTP(resp, req)
+			So(resp.Body.String(), ShouldEqual, "open 404.tmpl: no such file or directory\n")
+			So(resp.Code, ShouldEqual, 500)
+		})
+
 		Convey("Serve content", func() {
 		Convey("Serve content", func() {
 			m.Get("/content", func(ctx *Context) {
 			m.Get("/content", func(ctx *Context) {
 				ctx.ServeContent("content1", bytes.NewReader([]byte("Hello world!")))
 				ctx.ServeContent("content1", bytes.NewReader([]byte("Hello world!")))

+ 1 - 1
Godeps/_workspace/src/github.com/Unknwon/macaron/inject/inject_test.go

@@ -1,5 +1,5 @@
 // Copyright 2013 Martini Authors
 // Copyright 2013 Martini Authors
-// Copyright 2014 Unknown
+// Copyright 2014 Unknwon
 //
 //
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
 // not use this file except in compliance with the License. You may obtain
 // not use this file except in compliance with the License. You may obtain

+ 1 - 1
Godeps/_workspace/src/github.com/Unknwon/macaron/logger_test.go

@@ -46,7 +46,7 @@ func Test_Logger(t *testing.T) {
 		So(len(buf.String()), ShouldBeGreaterThan, 0)
 		So(len(buf.String()), ShouldBeGreaterThan, 0)
 	})
 	})
 
 
-	if !isWindows {
+	if ColorLog {
 		Convey("Color console output", t, func() {
 		Convey("Color console output", t, func() {
 			m := Classic()
 			m := Classic()
 			m.Get("/:code:int", func(ctx *Context) (int, string) {
 			m.Get("/:code:int", func(ctx *Context) (int, string) {

+ 2 - 2
Godeps/_workspace/src/github.com/Unknwon/macaron/macaron.go

@@ -29,7 +29,7 @@ import (
 	"github.com/Unknwon/macaron/inject"
 	"github.com/Unknwon/macaron/inject"
 )
 )
 
 
-const _VERSION = "0.5.0.0116"
+const _VERSION = "0.5.4.0318"
 
 
 func Version() string {
 func Version() string {
 	return _VERSION
 	return _VERSION
@@ -267,7 +267,7 @@ func SetConfig(source interface{}, others ...interface{}) (_ *ini.File, err erro
 // It returns an empty object if there is no one available.
 // It returns an empty object if there is no one available.
 func Config() *ini.File {
 func Config() *ini.File {
 	if cfg == nil {
 	if cfg == nil {
-		return &ini.File{}
+		return ini.Empty()
 	}
 	}
 	return cfg
 	return cfg
 }
 }

+ 37 - 5
Godeps/_workspace/src/github.com/Unknwon/macaron/render.go

@@ -1,4 +1,5 @@
 // Copyright 2013 Martini Authors
 // Copyright 2013 Martini Authors
+// Copyright 2013 oxtoacart
 // Copyright 2014 Unknwon
 // Copyright 2014 Unknwon
 //
 //
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
@@ -32,10 +33,39 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/Unknwon/com"
 	"github.com/Unknwon/com"
-
-	"github.com/Unknwon/macaron/bpool"
 )
 )
 
 
+// BufferPool implements a pool of bytes.Buffers in the form of a bounded channel.
+type BufferPool struct {
+	c chan *bytes.Buffer
+}
+
+// NewBufferPool creates a new BufferPool bounded to the given size.
+func NewBufferPool(size int) (bp *BufferPool) {
+	return &BufferPool{
+		c: make(chan *bytes.Buffer, size),
+	}
+}
+
+// Get gets a Buffer from the BufferPool, or creates a new one if none are available
+// in the pool.
+func (bp *BufferPool) Get() (b *bytes.Buffer) {
+	select {
+	case b = <-bp.c:
+	// reuse existing buffer
+	default:
+		// create new buffer
+		b = bytes.NewBuffer([]byte{})
+	}
+	return
+}
+
+// Put returns the given Buffer to the BufferPool.
+func (bp *BufferPool) Put(b *bytes.Buffer) {
+	b.Reset()
+	bp.c <- b
+}
+
 const (
 const (
 	ContentType    = "Content-Type"
 	ContentType    = "Content-Type"
 	ContentLength  = "Content-Length"
 	ContentLength  = "Content-Length"
@@ -50,7 +80,7 @@ const (
 
 
 var (
 var (
 	// Provides a temporary buffer to execute templates into and catch errors.
 	// Provides a temporary buffer to execute templates into and catch errors.
-	bufpool = bpool.NewBufferPool(64)
+	bufpool = NewBufferPool(64)
 
 
 	// Included helper functions for use when rendering html
 	// Included helper functions for use when rendering html
 	helperFuncs = template.FuncMap{
 	helperFuncs = template.FuncMap{
@@ -392,8 +422,10 @@ func (r *TplRender) RW() http.ResponseWriter {
 }
 }
 
 
 func (r *TplRender) JSON(status int, v interface{}) {
 func (r *TplRender) JSON(status int, v interface{}) {
-	var result []byte
-	var err error
+	var (
+		result []byte
+		err    error
+	)
 	if r.Opt.IndentJSON {
 	if r.Opt.IndentJSON {
 		result, err = json.MarshalIndent(v, "", "  ")
 		result, err = json.MarshalIndent(v, "", "  ")
 	} else {
 	} else {

+ 17 - 17
Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler.go

@@ -1,5 +1,5 @@
 // Copyright 2013 Martini Authors
 // Copyright 2013 Martini Authors
-// Copyright 2014 Unknown
+// Copyright 2014 Unknwon
 //
 //
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
 // not use this file except in compliance with the License. You may obtain
 // not use this file except in compliance with the License. You may obtain
@@ -28,32 +28,32 @@ import (
 // that are passed into this function.
 // that are passed into this function.
 type ReturnHandler func(*Context, []reflect.Value)
 type ReturnHandler func(*Context, []reflect.Value)
 
 
+func canDeref(val reflect.Value) bool {
+	return val.Kind() == reflect.Interface || val.Kind() == reflect.Ptr
+}
+
+func isByteSlice(val reflect.Value) bool {
+	return val.Kind() == reflect.Slice && val.Type().Elem().Kind() == reflect.Uint8
+}
+
 func defaultReturnHandler() ReturnHandler {
 func defaultReturnHandler() ReturnHandler {
 	return func(ctx *Context, vals []reflect.Value) {
 	return func(ctx *Context, vals []reflect.Value) {
 		rv := ctx.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
 		rv := ctx.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
 		res := rv.Interface().(http.ResponseWriter)
 		res := rv.Interface().(http.ResponseWriter)
-		var responseVal reflect.Value
+		var respVal reflect.Value
 		if len(vals) > 1 && vals[0].Kind() == reflect.Int {
 		if len(vals) > 1 && vals[0].Kind() == reflect.Int {
 			res.WriteHeader(int(vals[0].Int()))
 			res.WriteHeader(int(vals[0].Int()))
-			responseVal = vals[1]
+			respVal = vals[1]
 		} else if len(vals) > 0 {
 		} else if len(vals) > 0 {
-			responseVal = vals[0]
+			respVal = vals[0]
 		}
 		}
-		if canDeref(responseVal) {
-			responseVal = responseVal.Elem()
+		if canDeref(respVal) {
+			respVal = respVal.Elem()
 		}
 		}
-		if isByteSlice(responseVal) {
-			res.Write(responseVal.Bytes())
+		if isByteSlice(respVal) {
+			res.Write(respVal.Bytes())
 		} else {
 		} else {
-			res.Write([]byte(responseVal.String()))
+			res.Write([]byte(respVal.String()))
 		}
 		}
 	}
 	}
 }
 }
-
-func isByteSlice(val reflect.Value) bool {
-	return val.Kind() == reflect.Slice && val.Type().Elem().Kind() == reflect.Uint8
-}
-
-func canDeref(val reflect.Value) bool {
-	return val.Kind() == reflect.Interface || val.Kind() == reflect.Ptr
-}

+ 1 - 1
Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler_test.go

@@ -1,4 +1,4 @@
-// Copyright 2014 Unknown
+// Copyright 2014 Unknwon
 //
 //
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
 // not use this file except in compliance with the License. You may obtain
 // not use this file except in compliance with the License. You may obtain

+ 3 - 1
Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore

@@ -1 +1,3 @@
-testdata/conf_out.ini
+testdata/conf_out.ini
+ini.sublime-project
+ini.sublime-workspace

+ 34 - 2
Godeps/_workspace/src/gopkg.in/ini.v1/README.md

@@ -32,6 +32,12 @@ A **Data Source** is either raw data in type `[]byte` or a file name with type `
 cfg, err := ini.Load([]byte("raw data"), "filename")
 cfg, err := ini.Load([]byte("raw data"), "filename")
 ```
 ```
 
 
+Or start with an empty object:
+
+```go
+cfg := ini.Empty()
+```
+
 When you cannot decide how many data sources to load at the beginning, you still able to **Append()** them later.
 When you cannot decide how many data sources to load at the beginning, you still able to **Append()** them later.
 
 
 ```go
 ```go
@@ -58,7 +64,7 @@ When you're pretty sure the section exists, following code could make your life
 section := cfg.Section("")
 section := cfg.Section("")
 ```
 ```
 
 
-What happens when the section somehow does not exists? Won't panic, it returns an empty section object.
+What happens when the section somehow does not exist? Don't panic, it automatically creates and returns a new section to you.
 
 
 To create a new section:
 To create a new section:
 
 
@@ -117,6 +123,9 @@ val := cfg.Section("").Key("key name").String()
 To get value with types:
 To get value with types:
 
 
 ```go
 ```go
+// For boolean values:
+// true when value is: 1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On
+// false when value is: 0, f, F, FALSE, false, False, NO, no, No, OFF, off, Off
 v, err = cfg.Section("").Key("BOOL").Bool()
 v, err = cfg.Section("").Key("BOOL").Bool()
 v, err = cfg.Section("").Key("FLOAT64").Float64()
 v, err = cfg.Section("").Key("FLOAT64").Float64()
 v, err = cfg.Section("").Key("INT").Int()
 v, err = cfg.Section("").Key("INT").Int()
@@ -176,11 +185,22 @@ v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"})
 v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
 v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
 v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
 v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
 v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
 v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
-v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3})
+v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3})
+v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339
 ```
 ```
 
 
 Default value will be presented if value of key is not in candidates you given, and default value does not need be one of candidates.
 Default value will be presented if value of key is not in candidates you given, and default value does not need be one of candidates.
 
 
+To validate value in a given range:
+
+```go
+vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2)
+vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20)
+vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20)
+vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime)
+vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
+```
+
 To auto-split value into slice:
 To auto-split value into slice:
 
 
 ```go
 ```go
@@ -295,6 +315,18 @@ func main() {
 }
 }
 ```
 ```
 
 
+Can I have default value for field? Absolutely.
+
+Assign it before you map to struct. It will keep the value as it is if the key is not presented or got wrong type.
+
+```go
+// ...
+p := &Person{
+	Name: "Joe",
+}
+// ...
+```
+
 #### Name Mapper
 #### Name Mapper
 
 
 To save your time and make your code cleaner, this library supports [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) between struct field and actual secion and key name.
 To save your time and make your code cleaner, this library supports [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) between struct field and actual secion and key name.

+ 32 - 2
Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md

@@ -27,6 +27,12 @@
 cfg, err := ini.Load([]byte("raw data"), "filename")
 cfg, err := ini.Load([]byte("raw data"), "filename")
 ```
 ```
 
 
+或者从一个空白的文件开始:
+
+```go
+cfg := ini.Empty()
+```
+
 当您在一开始无法决定需要加载哪些数据源时,仍可以使用 **Append()** 在需要的时候加载它们。
 当您在一开始无法决定需要加载哪些数据源时,仍可以使用 **Append()** 在需要的时候加载它们。
 
 
 ```go
 ```go
@@ -53,7 +59,7 @@ section, err := cfg.GetSection("")
 section := cfg.Section("")
 section := cfg.Section("")
 ```
 ```
 
 
-如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会返回一个空的分区对象
+如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会自动创建并返回一个对应的分区对象给您
 
 
 创建一个分区:
 创建一个分区:
 
 
@@ -112,6 +118,9 @@ val := cfg.Section("").Key("key name").String()
 获取其它类型的值:
 获取其它类型的值:
 
 
 ```go
 ```go
+// 布尔值的规则:
+// true 当值为:1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On
+// false 当值为:0, f, F, FALSE, false, False, NO, no, No, OFF, off, Off
 v, err = cfg.Section("").Key("BOOL").Bool()
 v, err = cfg.Section("").Key("BOOL").Bool()
 v, err = cfg.Section("").Key("FLOAT64").Float64()
 v, err = cfg.Section("").Key("FLOAT64").Float64()
 v, err = cfg.Section("").Key("INT").Int()
 v, err = cfg.Section("").Key("INT").Int()
@@ -171,11 +180,22 @@ v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"})
 v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
 v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
 v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
 v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
 v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
 v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
-v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3})
+v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3})
+v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339
 ```
 ```
 
 
 如果获取到的值不是候选值的任意一个,则会返回默认值,而默认值不需要是候选值中的一员。
 如果获取到的值不是候选值的任意一个,则会返回默认值,而默认值不需要是候选值中的一员。
 
 
+验证获取的值是否在指定范围内:
+
+```go
+vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2)
+vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20)
+vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20)
+vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime)
+vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
+```
+
 自动分割键值为切片(slice):
 自动分割键值为切片(slice):
 
 
 ```go
 ```go
@@ -290,6 +310,16 @@ func main() {
 }
 }
 ```
 ```
 
 
+结构的字段怎么设置默认值呢?很简单,只要在映射之前对指定字段进行赋值就可以了。如果键未找到或者类型错误,该值不会发生改变。
+
+```go
+// ...
+p := &Person{
+	Name: "Joe",
+}
+// ...
+```
+
 #### 名称映射器(Name Mapper)
 #### 名称映射器(Name Mapper)
 
 
 为了节省您的时间并简化代码,本库支持类型为 [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) 的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。
 为了节省您的时间并简化代码,本库支持类型为 [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) 的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。

+ 99 - 7
Godeps/_workspace/src/gopkg.in/ini.v1/ini.go

@@ -35,7 +35,7 @@ const (
 	// Maximum allowed depth when recursively substituing variable names.
 	// Maximum allowed depth when recursively substituing variable names.
 	_DEPTH_VALUES = 99
 	_DEPTH_VALUES = 99
 
 
-	_VERSION = "1.0.1"
+	_VERSION = "1.2.6"
 )
 )
 
 
 func Version() string {
 func Version() string {
@@ -144,9 +144,24 @@ func (k *Key) String() string {
 	return val
 	return val
 }
 }
 
 
+// parseBool returns the boolean value represented by the string.
+//
+// It accepts 1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On,
+// 0, f, F, FALSE, false, False, NO, no, No, OFF, off, Off.
+// Any other value returns an error.
+func parseBool(str string) (value bool, err error) {
+	switch str {
+	case "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "ON", "on", "On":
+		return true, nil
+	case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "OFF", "off", "Off":
+		return false, nil
+	}
+	return false, fmt.Errorf("parsing \"%s\": invalid syntax", str)
+}
+
 // Bool returns bool type value.
 // Bool returns bool type value.
 func (k *Key) Bool() (bool, error) {
 func (k *Key) Bool() (bool, error) {
-	return strconv.ParseBool(k.String())
+	return parseBool(k.String())
 }
 }
 
 
 // Float64 returns float64 type value.
 // Float64 returns float64 type value.
@@ -305,6 +320,52 @@ func (k *Key) InTime(defaultVal time.Time, candidates []time.Time) time.Time {
 	return k.InTimeFormat(time.RFC3339, defaultVal, candidates)
 	return k.InTimeFormat(time.RFC3339, defaultVal, candidates)
 }
 }
 
 
+// RangeFloat64 checks if value is in given range inclusively,
+// and returns default value if it's not.
+func (k *Key) RangeFloat64(defaultVal, min, max float64) float64 {
+	val := k.MustFloat64()
+	if val < min || val > max {
+		return defaultVal
+	}
+	return val
+}
+
+// RangeInt checks if value is in given range inclusively,
+// and returns default value if it's not.
+func (k *Key) RangeInt(defaultVal, min, max int) int {
+	val := k.MustInt()
+	if val < min || val > max {
+		return defaultVal
+	}
+	return val
+}
+
+// RangeInt64 checks if value is in given range inclusively,
+// and returns default value if it's not.
+func (k *Key) RangeInt64(defaultVal, min, max int64) int64 {
+	val := k.MustInt64()
+	if val < min || val > max {
+		return defaultVal
+	}
+	return val
+}
+
+// RangeTimeFormat checks if value with given format is in given range inclusively,
+// and returns default value if it's not.
+func (k *Key) RangeTimeFormat(format string, defaultVal, min, max time.Time) time.Time {
+	val := k.MustTimeFormat(format)
+	if val.Unix() < min.Unix() || val.Unix() > max.Unix() {
+		return defaultVal
+	}
+	return val
+}
+
+// RangeTime checks if value with RFC3339 format is in given range inclusively,
+// and returns default value if it's not.
+func (k *Key) RangeTime(defaultVal, min, max time.Time) time.Time {
+	return k.RangeTimeFormat(time.RFC3339, defaultVal, min, max)
+}
+
 // Strings returns list of string devide by given delimiter.
 // Strings returns list of string devide by given delimiter.
 func (k *Key) Strings(delim string) []string {
 func (k *Key) Strings(delim string) []string {
 	str := k.String()
 	str := k.String()
@@ -440,7 +501,10 @@ func (s *Section) GetKey(name string) (*Key, error) {
 func (s *Section) Key(name string) *Key {
 func (s *Section) Key(name string) *Key {
 	key, err := s.GetKey(name)
 	key, err := s.GetKey(name)
 	if err != nil {
 	if err != nil {
-		return &Key{}
+		// It's OK here because the only possible error is empty key name,
+		// but if it's empty, this piece of code won't be executed.
+		key, _ = s.NewKey(name, "")
+		return key
 	}
 	}
 	return key
 	return key
 }
 }
@@ -555,6 +619,13 @@ func Load(source interface{}, others ...interface{}) (_ *File, err error) {
 	return f, f.Reload()
 	return f, f.Reload()
 }
 }
 
 
+// Empty returns an empty file object.
+func Empty() *File {
+	// Ignore error here, we sure our data is good.
+	f, _ := Load([]byte(""))
+	return f
+}
+
 // NewSection creates a new section.
 // NewSection creates a new section.
 func (f *File) NewSection(name string) (*Section, error) {
 func (f *File) NewSection(name string) (*Section, error) {
 	if len(name) == 0 {
 	if len(name) == 0 {
@@ -575,6 +646,16 @@ func (f *File) NewSection(name string) (*Section, error) {
 	return f.sections[name], nil
 	return f.sections[name], nil
 }
 }
 
 
+// NewSections creates a list of sections.
+func (f *File) NewSections(names ...string) (err error) {
+	for _, name := range names {
+		if _, err = f.NewSection(name); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 // GetSection returns section by given name.
 // GetSection returns section by given name.
 func (f *File) GetSection(name string) (*Section, error) {
 func (f *File) GetSection(name string) (*Section, error) {
 	if len(name) == 0 {
 	if len(name) == 0 {
@@ -597,7 +678,10 @@ func (f *File) GetSection(name string) (*Section, error) {
 func (f *File) Section(name string) *Section {
 func (f *File) Section(name string) *Section {
 	sec, err := f.GetSection(name)
 	sec, err := f.GetSection(name)
 	if err != nil {
 	if err != nil {
-		return newSection(f, name)
+		// It's OK here because the only possible error is empty section name,
+		// but if it's empty, this piece of code won't be executed.
+		sec, _ = f.NewSection(name)
+		return sec
 	}
 	}
 	return sec
 	return sec
 }
 }
@@ -638,6 +722,14 @@ func (f *File) DeleteSection(name string) {
 	}
 	}
 }
 }
 
 
+func cutComment(str string) string {
+	i := strings.Index(str, "#")
+	if i == -1 {
+		return str
+	}
+	return str[:i]
+}
+
 // parse parses data through an io.Reader.
 // parse parses data through an io.Reader.
 func (f *File) parse(reader io.Reader) error {
 func (f *File) parse(reader io.Reader) error {
 	buf := bufio.NewReader(reader)
 	buf := bufio.NewReader(reader)
@@ -776,7 +868,6 @@ func (f *File) parse(reader io.Reader) error {
 				val = lineRight[qLen:] + "\n"
 				val = lineRight[qLen:] + "\n"
 				for {
 				for {
 					next, err := buf.ReadString('\n')
 					next, err := buf.ReadString('\n')
-					val += next
 					if err != nil {
 					if err != nil {
 						if err != io.EOF {
 						if err != io.EOF {
 							return err
 							return err
@@ -785,9 +876,10 @@ func (f *File) parse(reader io.Reader) error {
 					}
 					}
 					pos = strings.LastIndex(next, valQuote)
 					pos = strings.LastIndex(next, valQuote)
 					if pos > -1 {
 					if pos > -1 {
-						val = val[:len(val)-len(valQuote)-1]
+						val += next[:pos]
 						break
 						break
 					}
 					}
+					val += next
 					if isEnd {
 					if isEnd {
 						return fmt.Errorf("error parsing line: missing closing key quote from '%s' to '%s'", line, next)
 						return fmt.Errorf("error parsing line: missing closing key quote from '%s' to '%s'", line, next)
 					}
 					}
@@ -796,7 +888,7 @@ func (f *File) parse(reader io.Reader) error {
 				val = lineRight[qLen : pos+qLen]
 				val = lineRight[qLen : pos+qLen]
 			}
 			}
 		} else {
 		} else {
-			val = strings.TrimSpace(lineRight[0:])
+			val = strings.TrimSpace(cutComment(lineRight[0:]))
 		}
 		}
 
 
 		k, err := section.NewKey(kname, val)
 		k, err := section.NewKey(kname, val)

+ 45 - 6
Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go

@@ -40,13 +40,13 @@ IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s
 # Information about package author
 # Information about package author
 # Bio can be written in multiple lines.
 # Bio can be written in multiple lines.
 [author]
 [author]
-NAME = Unknwon
+NAME = Unknwon  # Succeeding comment
 E-MAIL = fake@localhost
 E-MAIL = fake@localhost
 GITHUB = https://github.com/%(NAME)s
 GITHUB = https://github.com/%(NAME)s
 BIO = """Gopher.
 BIO = """Gopher.
 Coding addict.
 Coding addict.
 Good man.
 Good man.
-"""
+"""  # Succeeding comment
 
 
 [package]
 [package]
 CLONE_URL = https://%(IMPORT_PATH)s
 CLONE_URL = https://%(IMPORT_PATH)s
@@ -62,6 +62,7 @@ UNUSED_KEY = should be deleted
 [types]
 [types]
 STRING = str
 STRING = str
 BOOL = true
 BOOL = true
+BOOL_FALSE = false
 FLOAT64 = 1.25
 FLOAT64 = 1.25
 INT = 10
 INT = 10
 TIME = 2015-01-01T20:17:05Z
 TIME = 2015-01-01T20:17:05Z
@@ -88,9 +89,7 @@ func Test_Load(t *testing.T) {
 	Convey("Load from data sources", t, func() {
 	Convey("Load from data sources", t, func() {
 
 
 		Convey("Load with empty data", func() {
 		Convey("Load with empty data", func() {
-			cfg, err := Load([]byte(""))
-			So(err, ShouldBeNil)
-			So(cfg, ShouldNotBeNil)
+			So(Empty(), ShouldNotBeNil)
 		})
 		})
 
 
 		Convey("Load with multiple data sources", func() {
 		Convey("Load with multiple data sources", func() {
@@ -203,6 +202,10 @@ func Test_Values(t *testing.T) {
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 			So(v1, ShouldBeTrue)
 			So(v1, ShouldBeTrue)
 
 
+			v1, err = sec.Key("BOOL_FALSE").Bool()
+			So(err, ShouldBeNil)
+			So(v1, ShouldBeFalse)
+
 			v2, err := sec.Key("FLOAT64").Float64()
 			v2, err := sec.Key("FLOAT64").Float64()
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 			So(v2, ShouldEqual, 1.25)
 			So(v2, ShouldEqual, 1.25)
@@ -265,6 +268,30 @@ func Test_Values(t *testing.T) {
 			})
 			})
 		})
 		})
 
 
+		Convey("Get values in range", func() {
+			sec := cfg.Section("types")
+			So(sec.Key("FLOAT64").RangeFloat64(0, 1, 2), ShouldEqual, 1.25)
+			So(sec.Key("INT").RangeInt(0, 10, 20), ShouldEqual, 10)
+			So(sec.Key("INT").RangeInt64(0, 10, 20), ShouldEqual, 10)
+
+			minT, err := time.Parse(time.RFC3339, "0001-01-01T01:00:00Z")
+			So(err, ShouldBeNil)
+			midT, err := time.Parse(time.RFC3339, "2013-01-01T01:00:00Z")
+			So(err, ShouldBeNil)
+			maxT, err := time.Parse(time.RFC3339, "9999-01-01T01:00:00Z")
+			So(err, ShouldBeNil)
+			t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
+			So(err, ShouldBeNil)
+			So(sec.Key("TIME").RangeTime(t, minT, maxT).String(), ShouldEqual, t.String())
+
+			Convey("Get value in range with default value", func() {
+				So(sec.Key("FLOAT64").RangeFloat64(5, 0, 1), ShouldEqual, 5)
+				So(sec.Key("INT").RangeInt(7, 0, 5), ShouldEqual, 7)
+				So(sec.Key("INT").RangeInt64(7, 0, 5), ShouldEqual, 7)
+				So(sec.Key("TIME").RangeTime(t, minT, midT).String(), ShouldEqual, t.String())
+			})
+		})
+
 		Convey("Get values into slice", func() {
 		Convey("Get values into slice", func() {
 			sec := cfg.Section("array")
 			sec := cfg.Section("array")
 			So(strings.Join(sec.Key("STRINGS").Strings(","), ","), ShouldEqual, "en,zh,de")
 			So(strings.Join(sec.Key("STRINGS").Strings(","), ","), ShouldEqual, "en,zh,de")
@@ -304,7 +331,7 @@ func Test_Values(t *testing.T) {
 		})
 		})
 
 
 		Convey("Get key strings", func() {
 		Convey("Get key strings", func() {
-			So(strings.Join(cfg.Section("types").KeyStrings(), ","), ShouldEqual, "STRING,BOOL,FLOAT64,INT,TIME")
+			So(strings.Join(cfg.Section("types").KeyStrings(), ","), ShouldEqual, "STRING,BOOL,BOOL_FALSE,FLOAT64,INT,TIME")
 		})
 		})
 
 
 		Convey("Delete a key", func() {
 		Convey("Delete a key", func() {
@@ -321,6 +348,14 @@ func Test_Values(t *testing.T) {
 			cfg.DeleteSection("")
 			cfg.DeleteSection("")
 			So(cfg.SectionStrings()[0], ShouldNotEqual, DEFAULT_SECTION)
 			So(cfg.SectionStrings()[0], ShouldNotEqual, DEFAULT_SECTION)
 		})
 		})
+
+		Convey("Create new sections", func() {
+			cfg.NewSections("test", "test2")
+			_, err := cfg.GetSection("test")
+			So(err, ShouldBeNil)
+			_, err = cfg.GetSection("test2")
+			So(err, ShouldBeNil)
+		})
 	})
 	})
 
 
 	Convey("Test getting and setting bad values", t, func() {
 	Convey("Test getting and setting bad values", t, func() {
@@ -340,6 +375,10 @@ func Test_Values(t *testing.T) {
 			So(s, ShouldBeNil)
 			So(s, ShouldBeNil)
 		})
 		})
 
 
+		Convey("Create new sections with empty name", func() {
+			So(cfg.NewSections(""), ShouldNotBeNil)
+		})
+
 		Convey("Get section that not exists", func() {
 		Convey("Get section that not exists", func() {
 			s, err := cfg.GetSection("404")
 			s, err := cfg.GetSection("404")
 			So(err, ShouldNotBeNil)
 			So(err, ShouldNotBeNil)

+ 12 - 6
Godeps/_workspace/src/gopkg.in/ini.v1/struct.go

@@ -29,7 +29,7 @@ type NameMapper func(string) string
 var (
 var (
 	// AllCapsUnderscore converts to format ALL_CAPS_UNDERSCORE.
 	// AllCapsUnderscore converts to format ALL_CAPS_UNDERSCORE.
 	AllCapsUnderscore NameMapper = func(raw string) string {
 	AllCapsUnderscore NameMapper = func(raw string) string {
-		newstr := make([]rune, 0, 10)
+		newstr := make([]rune, 0, len(raw))
 		for i, chr := range raw {
 		for i, chr := range raw {
 			if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
 			if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
 				if i > 0 {
 				if i > 0 {
@@ -42,7 +42,7 @@ var (
 	}
 	}
 	// TitleUnderscore converts to format title_underscore.
 	// TitleUnderscore converts to format title_underscore.
 	TitleUnderscore NameMapper = func(raw string) string {
 	TitleUnderscore NameMapper = func(raw string) string {
-		newstr := make([]rune, 0, 10)
+		newstr := make([]rune, 0, len(raw))
 		for i, chr := range raw {
 		for i, chr := range raw {
 			if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
 			if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
 				if i > 0 {
 				if i > 0 {
@@ -75,32 +75,38 @@ func parseDelim(actual string) string {
 
 
 var reflectTime = reflect.TypeOf(time.Now()).Kind()
 var reflectTime = reflect.TypeOf(time.Now()).Kind()
 
 
+// setWithProperType sets proper value to field based on its type,
+// but it does not return error for failing parsing,
+// because we want to use default value that is already assigned to strcut.
 func setWithProperType(kind reflect.Kind, key *Key, field reflect.Value, delim string) error {
 func setWithProperType(kind reflect.Kind, key *Key, field reflect.Value, delim string) error {
 	switch kind {
 	switch kind {
 	case reflect.String:
 	case reflect.String:
+		if len(key.String()) == 0 {
+			return nil
+		}
 		field.SetString(key.String())
 		field.SetString(key.String())
 	case reflect.Bool:
 	case reflect.Bool:
 		boolVal, err := key.Bool()
 		boolVal, err := key.Bool()
 		if err != nil {
 		if err != nil {
-			return err
+			return nil
 		}
 		}
 		field.SetBool(boolVal)
 		field.SetBool(boolVal)
 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 		intVal, err := key.Int64()
 		intVal, err := key.Int64()
 		if err != nil {
 		if err != nil {
-			return err
+			return nil
 		}
 		}
 		field.SetInt(intVal)
 		field.SetInt(intVal)
 	case reflect.Float64:
 	case reflect.Float64:
 		floatVal, err := key.Float64()
 		floatVal, err := key.Float64()
 		if err != nil {
 		if err != nil {
-			return err
+			return nil
 		}
 		}
 		field.SetFloat(floatVal)
 		field.SetFloat(floatVal)
 	case reflectTime:
 	case reflectTime:
 		timeVal, err := key.Time()
 		timeVal, err := key.Time()
 		if err != nil {
 		if err != nil {
-			return err
+			return nil
 		}
 		}
 		field.Set(reflect.ValueOf(timeVal))
 		field.Set(reflect.ValueOf(timeVal))
 	case reflect.Slice:
 	case reflect.Slice:

+ 21 - 23
Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go

@@ -78,27 +78,17 @@ type unsupport4 struct {
 	*unsupport3 `ini:"Others"`
 	*unsupport3 `ini:"Others"`
 }
 }
 
 
-type invalidInt struct {
-	Age int
-}
-
-type invalidBool struct {
-	Male bool
-}
-
-type invalidFloat struct {
-	Money float64
-}
-
-type invalidTime struct {
-	Born time.Time
-}
-
-type emptySlice struct {
+type defaultValue struct {
+	Name   string
+	Age    int
+	Male   bool
+	Money  float64
+	Born   time.Time
 	Cities []string
 	Cities []string
 }
 }
 
 
 const _INVALID_DATA_CONF_STRUCT = `
 const _INVALID_DATA_CONF_STRUCT = `
+Name = 
 Age = age
 Age = age
 Male = 123
 Male = 123
 Money = money
 Money = money
@@ -154,12 +144,20 @@ func Test_Struct(t *testing.T) {
 		So(MapTo(&testStruct{}, "hi"), ShouldNotBeNil)
 		So(MapTo(&testStruct{}, "hi"), ShouldNotBeNil)
 	})
 	})
 
 
-	Convey("Map to wrong types", t, func() {
-		So(MapTo(&invalidInt{}, []byte(_INVALID_DATA_CONF_STRUCT)), ShouldNotBeNil)
-		So(MapTo(&invalidBool{}, []byte(_INVALID_DATA_CONF_STRUCT)), ShouldNotBeNil)
-		So(MapTo(&invalidFloat{}, []byte(_INVALID_DATA_CONF_STRUCT)), ShouldNotBeNil)
-		So(MapTo(&invalidTime{}, []byte(_INVALID_DATA_CONF_STRUCT)), ShouldNotBeNil)
-		So(MapTo(&emptySlice{}, []byte(_INVALID_DATA_CONF_STRUCT)), ShouldBeNil)
+	Convey("Map to wrong types and gain default values", t, func() {
+		cfg, err := Load([]byte(_INVALID_DATA_CONF_STRUCT))
+		So(err, ShouldBeNil)
+
+		t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
+		So(err, ShouldBeNil)
+		dv := &defaultValue{"Joe", 10, true, 1.25, t, []string{"HangZhou", "Boston"}}
+		So(cfg.MapTo(dv), ShouldBeNil)
+		So(dv.Name, ShouldEqual, "Joe")
+		So(dv.Age, ShouldEqual, 10)
+		So(dv.Male, ShouldBeTrue)
+		So(dv.Money, ShouldEqual, 1.25)
+		So(dv.Born.String(), ShouldEqual, t.String())
+		So(strings.Join(dv.Cities, ","), ShouldEqual, "HangZhou,Boston")
 	})
 	})
 }
 }
 
 

+ 2 - 2
docs/sources/guides/changes_in_v2.md

@@ -99,8 +99,8 @@ Here you can update your user details, UI Theme and change password.
 ## PNG rendering
 ## PNG rendering
 
 
 In the panel share dialog you now have access to a link that will render the panel to a PNG image.
 In the panel share dialog you now have access to a link that will render the panel to a PNG image.
-The panel is rendered on the backend via phantomjs (headless browser). This requires that you metric
-data source is accessable from your Grafana server host machine.
+The panel is rendered on the backend via phantomjs (headless browser). This requires that your metric
+data source is accessible from your Grafana server host machine.
 
 
 ![](/img/v2/share_dialog_image_highlight.jpg)
 ![](/img/v2/share_dialog_image_highlight.jpg)
 
 

+ 4 - 0
pkg/cmd/web.go

@@ -11,6 +11,7 @@ import (
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
 	"strconv"
 	"strconv"
+	"time"
 
 
 	"github.com/Unknwon/macaron"
 	"github.com/Unknwon/macaron"
 	"github.com/codegangsta/cli"
 	"github.com/codegangsta/cli"
@@ -68,6 +69,9 @@ func mapStatic(m *macaron.Macaron, dir string, prefix string) {
 		macaron.StaticOptions{
 		macaron.StaticOptions{
 			SkipLogging: true,
 			SkipLogging: true,
 			Prefix:      prefix,
 			Prefix:      prefix,
+			Expires: func() string {
+				return time.Now().UTC().Format(http.TimeFormat)
+			},
 		},
 		},
 	))
 	))
 }
 }

+ 8 - 0
pkg/models/dashboards.go

@@ -14,6 +14,7 @@ var (
 	ErrDashboardVersionMismatch    = errors.New("The dashboard has been changed by someone else")
 	ErrDashboardVersionMismatch    = errors.New("The dashboard has been changed by someone else")
 )
 )
 
 
+// Dashboard model
 type Dashboard struct {
 type Dashboard struct {
 	Id      int64
 	Id      int64
 	Slug    string
 	Slug    string
@@ -27,6 +28,7 @@ type Dashboard struct {
 	Data  map[string]interface{}
 	Data  map[string]interface{}
 }
 }
 
 
+// NewDashboard creates a new dashboard
 func NewDashboard(title string) *Dashboard {
 func NewDashboard(title string) *Dashboard {
 	dash := &Dashboard{}
 	dash := &Dashboard{}
 	dash.Data = make(map[string]interface{})
 	dash.Data = make(map[string]interface{})
@@ -36,6 +38,7 @@ func NewDashboard(title string) *Dashboard {
 	return dash
 	return dash
 }
 }
 
 
+// GetTags turns the tags in data json into go string array
 func (dash *Dashboard) GetTags() []string {
 func (dash *Dashboard) GetTags() []string {
 	jsonTags := dash.Data["tags"]
 	jsonTags := dash.Data["tags"]
 	if jsonTags == nil {
 	if jsonTags == nil {
@@ -50,6 +53,7 @@ func (dash *Dashboard) GetTags() []string {
 	return b
 	return b
 }
 }
 
 
+// GetDashboardModel turns the command into the savable model
 func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
 func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
 	dash := &Dashboard{}
 	dash := &Dashboard{}
 	dash.Data = cmd.Dashboard
 	dash.Data = cmd.Dashboard
@@ -63,15 +67,19 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
 		if dash.Data["version"] != nil {
 		if dash.Data["version"] != nil {
 			dash.Version = int(dash.Data["version"].(float64))
 			dash.Version = int(dash.Data["version"].(float64))
 		}
 		}
+	} else {
+		dash.Data["version"] = 0
 	}
 	}
 
 
 	return dash
 	return dash
 }
 }
 
 
+// GetString a
 func (dash *Dashboard) GetString(prop string) string {
 func (dash *Dashboard) GetString(prop string) string {
 	return dash.Data[prop].(string)
 	return dash.Data[prop].(string)
 }
 }
 
 
+// UpdateSlug updates the slug
 func (dash *Dashboard) UpdateSlug() {
 func (dash *Dashboard) UpdateSlug() {
 	title := strings.ToLower(dash.Data["title"].(string))
 	title := strings.ToLower(dash.Data["title"].(string))
 	re := regexp.MustCompile("[^\\w ]+")
 	re := regexp.MustCompile("[^\\w ]+")

+ 1 - 1
pkg/services/sqlstore/migrations/apikey_mig.go

@@ -42,7 +42,7 @@ func addApiKeyMigrations(mg *Migrator) {
 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
 			{Name: "org_id", Type: DB_BigInt, Nullable: false},
 			{Name: "org_id", Type: DB_BigInt, Nullable: false},
 			{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
 			{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
-			{Name: "key", Type: DB_Varchar, Length: 64, Nullable: false},
+			{Name: "key", Type: DB_Varchar, Length: 255, Nullable: false},
 			{Name: "role", Type: DB_NVarchar, Length: 255, Nullable: false},
 			{Name: "role", Type: DB_NVarchar, Length: 255, Nullable: false},
 			{Name: "created", Type: DB_DateTime, Nullable: false},
 			{Name: "created", Type: DB_DateTime, Nullable: false},
 			{Name: "updated", Type: DB_DateTime, Nullable: false},
 			{Name: "updated", Type: DB_DateTime, Nullable: false},

+ 6 - 0
pkg/services/sqlstore/user.go

@@ -56,6 +56,12 @@ func getOrgIdForNewUser(userEmail string, sess *session) (int64, error) {
 		return 0, err
 		return 0, err
 	}
 	}
 
 
+	sess.publishAfterCommit(&events.OrgCreated{
+		Timestamp: org.Created,
+		Id:        org.Id,
+		Name:      org.Name,
+	})
+
 	return org.Id, nil
 	return org.Id, nil
 }
 }
 
 

+ 0 - 19
src/app/controllers/search.js

@@ -136,25 +136,6 @@ function (angular, _, config) {
 
 
   });
   });
 
 
-  module.directive('xngFocus', function() {
-    return function(scope, element, attrs) {
-      element.click(function(e) {
-        e.stopPropagation();
-      });
-
-      scope.$watch(attrs.xngFocus,function (newValue) {
-        if (!newValue) {
-          return;
-        }
-        setTimeout(function() {
-          element.focus();
-          var pos = element.val().length * 2;
-          element[0].setSelectionRange(pos, pos);
-        }, 200);
-      },true);
-    };
-  });
-
   module.directive('tagColorFromName', function() {
   module.directive('tagColorFromName', function() {
 
 
     function djb2(str) {
     function djb2(str) {

+ 1 - 0
src/app/directives/all.js

@@ -16,4 +16,5 @@ define([
   './grafanaVersionCheck',
   './grafanaVersionCheck',
   './dropdown.typeahead',
   './dropdown.typeahead',
   './topnav',
   './topnav',
+  './giveFocus',
 ], function () {});
 ], function () {});

+ 26 - 0
src/app/directives/giveFocus.js

@@ -0,0 +1,26 @@
+define([
+  'angular',
+],
+function (angular) {
+  'use strict';
+
+  var module = angular.module('grafana.directives');
+
+  module.directive('giveFocus', function() {
+    return function(scope, element, attrs) {
+      element.click(function(e) {
+        e.stopPropagation();
+      });
+
+      scope.$watch(attrs.giveFocus, function (newValue) {
+        if (!newValue) { return; }
+
+        setTimeout(function() {
+          element.focus();
+          var pos = element.val().length * 2;
+          element[0].setSelectionRange(pos, pos);
+        }, 200);
+      },true);
+    };
+  });
+});

+ 110 - 0
src/app/directives/templateParamSelector.js

@@ -84,4 +84,114 @@ function (angular, app, _, $) {
         }
         }
       };
       };
     });
     });
+
+  angular
+    .module('grafana.directives')
+    .directive('variableValueSelect', function($compile, $window, $timeout) {
+      return {
+        scope: {
+          variable: "=",
+          onUpdated: "&"
+        },
+        templateUrl: 'app/features/dashboard/partials/variableValueSelect.html',
+        link: function(scope, elem) {
+          var bodyEl = angular.element($window.document.body);
+          var variable = scope.variable;
+
+          scope.show = function() {
+            scope.selectorOpen = true;
+            scope.giveFocus = 1;
+            scope.oldCurrentText = variable.current.text;
+
+            var currentValues = variable.current.value;
+
+            if (_.isString(currentValues)) {
+              currentValues  = [currentValues];
+            }
+
+            scope.options = _.map(variable.options, function(option) {
+              var op = {text: option.text, value: option.value};
+              if (_.indexOf(currentValues, option.value) >= 0) {
+                op.selected = true;
+              }
+              return op;
+            });
+
+            $timeout(function() {
+              bodyEl.on('click', scope.bodyOnClick);
+            }, 0, false);
+          };
+
+          scope.optionSelected = function(option) {
+            option.selected = !option.selected;
+
+            if (!variable.multi || option.text === 'All') {
+              _.each(scope.options, function(other) {
+                if (option !== other) {
+                  other.selected = false;
+                }
+              });
+            }
+
+            var selected = _.filter(scope.options, {selected: true});
+
+            // enfore the first selected if no option is selected
+            if (selected.length === 0) {
+              scope.options[0].selected = true;
+              selected = [scope.options[0]];
+            }
+
+            if (selected.length > 1) {
+              if (selected[0].text === 'All') {
+                selected[0].selected = false;
+                selected = selected.slice(1, selected.length);
+              }
+            }
+
+            variable.current = {
+              text: _.pluck(selected, 'text').join(', '),
+              value: _.pluck(selected, 'value'),
+            };
+
+            // only single value
+            if (variable.current.value.length === 1) {
+              variable.current.value = selected[0].value;
+            }
+
+            scope.updateLinkText();
+            scope.onUpdated();
+          };
+
+          scope.hide = function() {
+            scope.selectorOpen = false;
+            if (scope.oldCurrentText !== variable.current.text) {
+              scope.onUpdated();
+            }
+
+            bodyEl.off('click', scope.bodyOnClick);
+          };
+
+          scope.bodyOnClick = function(e) {
+            var dropdown = elem.find('.variable-value-dropdown');
+            if (dropdown.has(e.target).length === 0) {
+              scope.$apply(scope.hide);
+            }
+          };
+
+          scope.updateLinkText = function() {
+            scope.linkText = "";
+            if (!variable.hideLabel) {
+              scope.linkText = (variable.label || variable.name) + ': ';
+            }
+
+            scope.linkText += variable.current.text;
+          };
+
+          scope.$watchGroup(['variable.hideLabel', 'variable.name', 'variable.label', 'variable.current.text'], function() {
+            scope.updateLinkText();
+          });
+        },
+      };
+    });
+
 });
 });

+ 3 - 2
src/app/directives/tip.js

@@ -57,14 +57,15 @@ function (angular, kbn) {
 
 
   angular
   angular
     .module('grafana.directives')
     .module('grafana.directives')
-    .directive('editorCheckbox', function($compile) {
+    .directive('editorCheckbox', function($compile, $interpolate) {
       return {
       return {
         restrict: 'E',
         restrict: 'E',
         link: function(scope, elem, attrs) {
         link: function(scope, elem, attrs) {
+          var text = $interpolate(attrs.text)(scope);
           var ngchange = attrs.change ? (' ng-change="' + attrs.change + '"') : '';
           var ngchange = attrs.change ? (' ng-change="' + attrs.change + '"') : '';
           var tip = attrs.tip ? (' <tip>' + attrs.tip + '</tip>') : '';
           var tip = attrs.tip ? (' <tip>' + attrs.tip + '</tip>') : '';
           var label = '<label for="' + scope.$id + attrs.model + '" class="checkbox-label">' +
           var label = '<label for="' + scope.$id + attrs.model + '" class="checkbox-label">' +
-                           attrs.text + tip + '</label>';
+                           text + tip + '</label>';
 
 
           var template = '<input class="cr1" id="' + scope.$id + attrs.model + '" type="checkbox" ' +
           var template = '<input class="cr1" id="' + scope.$id + attrs.model + '" type="checkbox" ' +
                           '       ng-model="' + attrs.model + '"' + ngchange +
                           '       ng-model="' + attrs.model + '"' + ngchange +

+ 3 - 3
src/app/features/dashboard/partials/shareModal.html

@@ -20,17 +20,17 @@
 		<br>
 		<br>
 		<div class="gf-form">
 		<div class="gf-form">
 			<div class="gf-form-row">
 			<div class="gf-form-row">
-					<editor-checkbox text="Current time range" model="forCurrent" change="buildUrl()"></editor-checkbox>
+					<editor-checkbox text="Current time range" model="options.forCurrent" change="buildUrl()"></editor-checkbox>
 				</div>
 				</div>
 			</div>
 			</div>
 			<div class="gf-form" ng-if="panel">
 			<div class="gf-form" ng-if="panel">
 				<div class="gf-form-row">
 				<div class="gf-form-row">
-					<editor-checkbox text="This panel only" model="toPanel" change="buildUrl()"></editor-checkbox>
+					<editor-checkbox text="This panel only" model="options.toPanel" change="buildUrl()"></editor-checkbox>
 				</div>
 				</div>
 			</div>
 			</div>
 			<div class="gf-form">
 			<div class="gf-form">
 				<div class="gf-form-row">
 				<div class="gf-form-row">
-					<editor-checkbox text="Include template variables" model="includeTemplateVars" change="buildUrl()"></editor-checkbox>
+					<editor-checkbox text="Include template variables" model="options.includeTemplateVars" change="buildUrl()"></editor-checkbox>
 				</div>
 				</div>
 			</div>
 			</div>
 
 

+ 24 - 0
src/app/features/dashboard/partials/variableValueSelect.html

@@ -0,0 +1,24 @@
+<a ng-click="show()" class="variable-value-link">
+	{{linkText}}
+	<i class="fa fa-caret-down"></i>
+</a>
+
+<div ng-if="selectorOpen" class="variable-value-dropdown">
+	<div class="search-field-wrapper">
+		<span style="position: relative;">
+			<input  type="text" placeholder="Search variable values" give-focus="giveFocus" tabindex="1"
+			ng-keydown="keyDown($event)" ng-model="query.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="search()" />
+		</span>
+	</div>
+
+	<div class="variable-options-container" ng-if="!query.tagcloud">
+		<a class="variable-option pointer" bindonce ng-repeat="option in options"
+				ng-class="{'selected': option.selected}" ng-click="optionSelected(option)">
+				<i class="fa fa-fw fa-square-o"></i>
+				<i class="fa fa-fw fa-check-square-o"></i>
+				<span >{{option.text}}</label>
+			</div>
+		</a>
+	</div>
+</div>
+

+ 8 - 10
src/app/features/dashboard/sharePanelCtrl.js

@@ -13,15 +13,13 @@ function (angular, _, require, config) {
 
 
     $scope.init = function() {
     $scope.init = function() {
       $scope.editor = { index: 0 };
       $scope.editor = { index: 0 };
-      $scope.forCurrent = true;
+      $scope.options = {
+        forCurrent: true,
+        toPanel: $scope.panel ? true : false,
+        includeTemplateVars: true
+      };
 
 
-      if ($scope.panel) {
-        $scope.toPanel = true;
-      }
-
-      $scope.includeTemplateVars = true;
       $scope.buildUrl();
       $scope.buildUrl();
-
     };
     };
 
 
     $scope.buildUrl = function() {
     $scope.buildUrl = function() {
@@ -38,7 +36,7 @@ function (angular, _, require, config) {
       params.from = range.from;
       params.from = range.from;
       params.to = range.to;
       params.to = range.to;
 
 
-      if ($scope.includeTemplateVars) {
+      if ($scope.options.includeTemplateVars) {
         _.each(templateSrv.variables, function(variable) {
         _.each(templateSrv.variables, function(variable) {
           params['var-' + variable.name] = variable.current.text;
           params['var-' + variable.name] = variable.current.text;
         });
         });
@@ -49,12 +47,12 @@ function (angular, _, require, config) {
         });
         });
       }
       }
 
 
-      if (!$scope.forCurrent) {
+      if (!$scope.options.forCurrent) {
         delete params.from;
         delete params.from;
         delete params.to;
         delete params.to;
       }
       }
 
 
-      if ($scope.toPanel) {
+      if ($scope.options.toPanel) {
         params.panelId = $scope.panel.id;
         params.panelId = $scope.panel.id;
         params.fullscreen = true;
         params.fullscreen = true;
       } else {
       } else {

+ 2 - 2
src/app/features/dashboard/submenuCtrl.js

@@ -26,8 +26,8 @@ function (angular, _) {
       $rootScope.$broadcast('refresh');
       $rootScope.$broadcast('refresh');
     };
     };
 
 
-    $scope.setVariableValue = function(param, option) {
-      templateValuesSrv.setVariableValue(param, option).then(function() {
+    $scope.variableUpdated = function(variable) {
+      templateValuesSrv.variableUpdated(variable).then(function() {
         dynamicDashboardSrv.update($scope.dashboard);
         dynamicDashboardSrv.update($scope.dashboard);
         $rootScope.$broadcast('refresh');
         $rootScope.$broadcast('refresh');
       });
       });

+ 2 - 2
src/app/features/panel/panelDirective.js

@@ -30,8 +30,8 @@ function (angular, $, config) {
         link: function(scope, elem) {
         link: function(scope, elem) {
           var panelContainer = elem.find('.panel-container');
           var panelContainer = elem.find('.panel-container');
 
 
-          scope.$watchGroup(['fullscreen', 'panel.height', 'row.height'], function() {
-            panelContainer.css({ minHeight: scope.panel.height || scope.row.height, display: 'block' });
+          scope.$watchGroup(['fullscreen', 'height', 'panel.height', 'row.height'], function() {
+            panelContainer.css({ minHeight: scope.height || scope.panel.height || scope.row.height, display: 'block' });
             elem.toggleClass('panel-fullscreen', scope.fullscreen ? true : false);
             elem.toggleClass('panel-fullscreen', scope.fullscreen ? true : false);
           });
           });
         }
         }

+ 3 - 1
src/app/features/templating/editorCtrl.js

@@ -17,6 +17,8 @@ function (angular, _) {
       options: [],
       options: [],
       includeAll: false,
       includeAll: false,
       allFormat: 'glob',
       allFormat: 'glob',
+      multi: false,
+      multiFormat: 'glob',
     };
     };
 
 
     $scope.init = function() {
     $scope.init = function() {
@@ -75,7 +77,7 @@ function (angular, _) {
       if ($scope.current.datasource === void 0) {
       if ($scope.current.datasource === void 0) {
         $scope.current.datasource = null;
         $scope.current.datasource = null;
         $scope.current.type = 'query';
         $scope.current.type = 'query';
-        $scope.current.allFormat = 'Glob';
+        $scope.current.allFormat = 'glob';
       }
       }
     };
     };
 
 

+ 13 - 1
src/app/features/templating/templateSrv.js

@@ -29,11 +29,23 @@ function (angular, _) {
       _.each(this.variables, function(variable) {
       _.each(this.variables, function(variable) {
         if (!variable.current || !variable.current.value) { return; }
         if (!variable.current || !variable.current.value) { return; }
 
 
-        this._values[variable.name] = variable.current.value;
+        this._values[variable.name] = this.renderVariableValue(variable);
         this._texts[variable.name] = variable.current.text;
         this._texts[variable.name] = variable.current.text;
       }, this);
       }, this);
     };
     };
 
 
+    this.renderVariableValue = function(variable) {
+      var value = variable.current.value;
+      if (_.isString(value)) {
+        return value;
+      } else {
+        if (variable.multiFormat === 'regex values') {
+          return '(' + value.join('|') + ')';
+        }
+        return '{' + value.join(',') + '}';
+      }
+    };
+
     this.setGrafanaVariable = function (name, value) {
     this.setGrafanaVariable = function (name, value) {
       this._grafanaVariables[name] = value;
       this._grafanaVariables[name] = value;
     };
     };

+ 5 - 0
src/app/features/templating/templateValuesSrv.js

@@ -66,6 +66,11 @@ function (angular, _, kbn) {
       return this.updateOptionsInChildVariables(variable);
       return this.updateOptionsInChildVariables(variable);
     };
     };
 
 
+    this.variableUpdated = function(variable) {
+      templateSrv.updateTemplateData();
+      return this.updateOptionsInChildVariables(variable);
+    };
+
     this.updateOptionsInChildVariables = function(updatedVariable) {
     this.updateOptionsInChildVariables = function(updatedVariable) {
       var promises = _.map(self.variables, function(otherVariable) {
       var promises = _.map(self.variables, function(otherVariable) {
         if (otherVariable === updatedVariable) {
         if (otherVariable === updatedVariable) {

+ 12 - 0
src/app/panels/graph/axisEditor.html

@@ -30,6 +30,12 @@
 					empty-to-null ng-model="panel.grid.leftMin"
 					empty-to-null ng-model="panel.grid.leftMin"
 					ng-change="render()" ng-model-onblur>
 					ng-change="render()" ng-model-onblur>
 				</li>
 				</li>
+				<li class="tight-form-item">
+					Scale type
+				</li>
+				<li>
+					<select class="input-small tight-form-input" style="width: 113px" ng-model="panel.grid.leftLogBase" ng-options="v as k for (k, v) in logScales" ng-change="render()"></select>
+				</li>
 				<li class="tight-form-item">
 				<li class="tight-form-item">
 					Label
 					Label
 				</li>
 				</li>
@@ -69,6 +75,12 @@
 					empty-to-null ng-model="panel.grid.rightMin"
 					empty-to-null ng-model="panel.grid.rightMin"
 					ng-change="render()" ng-model-onblur>
 					ng-change="render()" ng-model-onblur>
 				</li>
 				</li>
+				<li class="tight-form-item">
+					Scale type
+				</li>
+				<li>
+					<select class="input-small tight-form-input" style="width: 113px" ng-model="panel.grid.rightLogBase" ng-options="v as k for (k, v) in logScales" ng-change="render()"></select>
+				</li>
 				<li class="tight-form-item">
 				<li class="tight-form-item">
 					Label
 					Label
 				</li>
 				</li>

+ 89 - 42
src/app/panels/graph/graph.js

@@ -27,6 +27,7 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
         var dashboard = scope.dashboard;
         var dashboard = scope.dashboard;
         var data, annotations;
         var data, annotations;
         var sortedSeries;
         var sortedSeries;
+        var graphHeight;
         var legendSideLastValue = null;
         var legendSideLastValue = null;
         scope.crosshairEmiter = false;
         scope.crosshairEmiter = false;
 
 
@@ -64,19 +65,19 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
 
 
         function setElementHeight() {
         function setElementHeight() {
           try {
           try {
-            var height = scope.height || scope.panel.height || scope.row.height;
-            if (_.isString(height)) {
-              height = parseInt(height.replace('px', ''), 10);
+            graphHeight = scope.height || scope.panel.height || scope.row.height;
+            if (_.isString(graphHeight)) {
+              graphHeight = parseInt(graphHeight.replace('px', ''), 10);
             }
             }
 
 
-            height -= 5; // padding
-            height -= scope.panel.title ? 24 : 9; // subtract panel title bar
+            graphHeight -= 5; // padding
+            graphHeight -= scope.panel.title ? 24 : 9; // subtract panel title bar
 
 
             if (scope.panel.legend.show && !scope.panel.legend.rightSide) {
             if (scope.panel.legend.show && !scope.panel.legend.rightSide) {
-              height = height - 26; // subtract one line legend
+              graphHeight = graphHeight - 26; // subtract one line legend
             }
             }
 
 
-            elem.css('height', height + 'px');
+            elem.css('height', graphHeight + 'px');
 
 
             return true;
             return true;
           } catch(e) { // IE throws errors sometimes
           } catch(e) { // IE throws errors sometimes
@@ -349,6 +350,8 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
             position: 'left',
             position: 'left',
             show: scope.panel['y-axis'],
             show: scope.panel['y-axis'],
             min: scope.panel.grid.leftMin,
             min: scope.panel.grid.leftMin,
+            index: 1,
+            logBase: scope.panel.grid.leftLogBase,
             max: scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.leftMax,
             max: scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.leftMax,
           };
           };
 
 
@@ -356,16 +359,60 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
 
 
           if (_.findWhere(data, {yaxis: 2})) {
           if (_.findWhere(data, {yaxis: 2})) {
             var secondY = _.clone(defaults);
             var secondY = _.clone(defaults);
+            secondY.index = 2,
+            secondY.logBase = scope.panel.grid.rightLogBase;
             secondY.position = 'right';
             secondY.position = 'right';
             secondY.min = scope.panel.grid.rightMin;
             secondY.min = scope.panel.grid.rightMin;
             secondY.max = scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.rightMax;
             secondY.max = scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.rightMax;
             options.yaxes.push(secondY);
             options.yaxes.push(secondY);
+
+            applyLogScale(options.yaxes[1], data);
             configureAxisMode(options.yaxes[1], scope.panel.y_formats[1]);
             configureAxisMode(options.yaxes[1], scope.panel.y_formats[1]);
           }
           }
 
 
+          applyLogScale(options.yaxes[0], data);
           configureAxisMode(options.yaxes[0], scope.panel.y_formats[0]);
           configureAxisMode(options.yaxes[0], scope.panel.y_formats[0]);
         }
         }
 
 
+        function applyLogScale(axis, data) {
+          if (axis.logBase !== 10) {
+            return;
+          }
+
+          var series, i;
+          var max = axis.max;
+
+          if (max === null) {
+            for (i = 0; i < data.length; i++) {
+              series = data[i];
+              if (series.yaxis === axis.index) {
+                if (max < series.stats.max) {
+                  max = series.stats.max;
+                }
+              }
+            }
+
+            if (max === null) {
+              max = Number.MAX_VALUE;
+            }
+          }
+
+          axis.min = axis.min !== null ? axis.min : 1;
+          axis.ticks = [1];
+          var tick = 1;
+
+          while (true) {
+            tick = tick * axis.logBase;
+            axis.ticks.push(tick);
+            if (tick > max) {
+              break;
+            }
+          }
+
+          axis.transform = function(v) { return Math.log(v+0.001); };
+          axis.inverseTransform  = function (v) { return Math.pow(10,v); };
+        }
+
         function configureAxisMode(axis, format) {
         function configureAxisMode(axis, format) {
           axis.tickFormatter = function(val, axis) {
           axis.tickFormatter = function(val, axis) {
             return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
             return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
@@ -411,44 +458,44 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
           url += scope.panel['y-axis'] ? '' : '&hideYAxis=true';
           url += scope.panel['y-axis'] ? '' : '&hideYAxis=true';
 
 
           switch(scope.panel.y_formats[0]) {
           switch(scope.panel.y_formats[0]) {
-          case 'bytes':
-            url += '&yUnitSystem=binary';
-            break;
-          case 'bits':
-            url += '&yUnitSystem=binary';
-            break;
-          case 'bps':
-            url += '&yUnitSystem=si';
-            break;
-          case 'Bps':
-            url += '&yUnitSystem=si';
-            break;
-          case 'short':
-            url += '&yUnitSystem=si';
-            break;
-          case 'joule':
-            url += '&yUnitSystem=si';
-            break;
-          case 'watt':
-            url += '&yUnitSystem=si';
-            break;
-          case 'ev':
-            url += '&yUnitSystem=si';
-            break;
-          case 'none':
-            url += '&yUnitSystem=none';
-            break;
+            case 'bytes':
+              url += '&yUnitSystem=binary';
+              break;
+            case 'bits':
+              url += '&yUnitSystem=binary';
+              break;
+            case 'bps':
+              url += '&yUnitSystem=si';
+              break;
+            case 'Bps':
+              url += '&yUnitSystem=si';
+              break;
+            case 'short':
+              url += '&yUnitSystem=si';
+              break;
+            case 'joule':
+              url += '&yUnitSystem=si';
+              break;
+            case 'watt':
+              url += '&yUnitSystem=si';
+              break;
+            case 'ev':
+              url += '&yUnitSystem=si';
+              break;
+            case 'none':
+              url += '&yUnitSystem=none';
+              break;
           }
           }
 
 
           switch(scope.panel.nullPointMode) {
           switch(scope.panel.nullPointMode) {
-          case 'connected':
-            url += '&lineMode=connected';
-            break;
-          case 'null':
-            break; // graphite default lineMode
-          case 'null as zero':
-            url += "&drawNullAsZero=true";
-            break;
+            case 'connected':
+              url += '&lineMode=connected';
+              break;
+            case 'null':
+              break; // graphite default lineMode
+            case 'null as zero':
+              url += "&drawNullAsZero=true";
+              break;
           }
           }
 
 
           url += scope.panel.steppedLine ? '&lineMode=staircase' : '';
           url += scope.panel.steppedLine ? '&lineMode=staircase' : '';

+ 1 - 1
src/app/panels/graph/graph.tooltip.js

@@ -99,7 +99,7 @@ function ($) {
       var group, value, timestamp, hoverInfo, i, series, seriesHtml;
       var group, value, timestamp, hoverInfo, i, series, seriesHtml;
 
 
       if(dashboard.sharedCrosshair){
       if(dashboard.sharedCrosshair){
-        scope.appEvent('setCrosshair',  { pos: pos, scope: scope });
+        scope.appEvent('setCrosshair', { pos: pos, scope: scope });
       }
       }
 
 
       if (seriesList.length === 0) {
       if (seriesList.length === 0) {

+ 5 - 1
src/app/panels/graph/module.js

@@ -53,10 +53,12 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
       y_formats    : ['short', 'short'],
       y_formats    : ['short', 'short'],
       // grid options
       // grid options
       grid          : {
       grid          : {
+        leftLogBase: 1,
         leftMax: null,
         leftMax: null,
         rightMax: null,
         rightMax: null,
         leftMin: null,
         leftMin: null,
         rightMin: null,
         rightMin: null,
+        rightLogBase: 1,
         threshold1: null,
         threshold1: null,
         threshold2: null,
         threshold2: null,
         threshold1Color: 'rgba(216, 200, 27, 0.27)',
         threshold1Color: 'rgba(216, 200, 27, 0.27)',
@@ -95,7 +97,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
       // tooltip options
       // tooltip options
       tooltip       : {
       tooltip       : {
         value_type: 'cumulative',
         value_type: 'cumulative',
-        shared: false,
+        shared: true,
       },
       },
       // time overrides
       // time overrides
       timeFrom: null,
       timeFrom: null,
@@ -114,6 +116,8 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
     _.defaults($scope.panel.grid, _d.grid);
     _.defaults($scope.panel.grid, _d.grid);
     _.defaults($scope.panel.legend, _d.legend);
     _.defaults($scope.panel.legend, _d.legend);
 
 
+    $scope.logScales = {'linear': 1, 'log (base 10)': 10};
+
     $scope.hiddenSeries = {};
     $scope.hiddenSeries = {};
     $scope.seriesList = [];
     $scope.seriesList = [];
     $scope.unitFormats = kbn.getUnitFormats();
     $scope.unitFormats = kbn.getUnitFormats();

+ 0 - 1
src/app/partials/dasheditor.html

@@ -31,7 +31,6 @@
 					</div>
 					</div>
 					<editor-opt-bool text="Hide controls (CTRL+H)" model="dashboard.hideControls"></editor-opt-bool>
 					<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>
           <editor-opt-bool text="Shared Crosshair (CTRL+O)" model="dashboard.sharedCrosshair"></editor-opt-bool>
-					<editor-opt-bool text="Editable" model="dashboard.editable"></editor-opt-bool>
 				</div>
 				</div>
 			</div>
 			</div>
 			<div class="editor-row">
 			<div class="editor-row">

+ 1 - 1
src/app/partials/search.html

@@ -2,7 +2,7 @@
 
 
 	<div class="search-field-wrapper">
 	<div class="search-field-wrapper">
 		<span style="position: relative;">
 		<span style="position: relative;">
-			<input  type="text" placeholder="Find dashboards by name" xng-focus="giveSearchFocus" tabindex="1"
+			<input  type="text" placeholder="Find dashboards by name" give-focus="giveSearchFocus" tabindex="1"
 			ng-keydown="keyDown($event)" ng-model="query.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="search()" />
 			ng-keydown="keyDown($event)" ng-model="query.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="search()" />
 		</span>
 		</span>
 		<div class="search-switches">
 		<div class="search-switches">

+ 17 - 8
src/app/partials/submenu.html

@@ -1,19 +1,28 @@
 <div class="submenu-controls" ng-controller="SubmenuCtrl">
 <div class="submenu-controls" ng-controller="SubmenuCtrl">
 	<div class="tight-form borderless">
 	<div class="tight-form borderless">
 
 
-
 		<ul class="tight-form-list" ng-if="dashboard.templating.list.length > 0">
 		<ul class="tight-form-list" ng-if="dashboard.templating.list.length > 0">
-			<li class="tight-form-item">
-				<strong>VARIABLES:</strong>
-			</li>
-			<li ng-repeat-start="variable in variables" class="tight-form-item template-param-name">
-				<span class="template-variable ">
-					${{variable.name}}:
-				</span>
+
+			<li ng-repeat="variable in variables" class="tight-form-item template-param-name dropdown">
+				<variable-value-select variable="variable" on-updated="variableUpdated(variable)"></variable-value-select>
 			</li>
 			</li>
 
 
+			<!-- <li class="dropdown" ng&#45;repeat&#45;end> -->
+			<!-- 	<a class="tight&#45;form&#45;item" tabindex="1" data&#45;toggle="dropdown">{{variable.current.text}} <i class="fa fa&#45;caret&#45;down"></i></a> -->
+			<!-- 	<div class="dropdown&#45;menu variable&#45;values&#45;dropdown"> -->
+			<!-- 		<input type="text" class="fluid&#45;width"> -->
+			<!-- 		<div class="variable&#45;values&#45;list"> -->
+			<!-- 			<div class="variable&#45;values&#45;list&#45;item" ng&#45;repeat="option in variable.options"> -->
+			<!-- 				<editor&#45;checkbox text="{{option.text}}" model="asd" change="buildUrl()"></editor&#45;checkbox> -->
+			<!-- 			</div> -->
+			<!-- 		</div> -->
+			<!-- 	</div> -->
+			<!-- </li> -->
+      <!--  -->
+			<!--
 			<li ng-repeat-end template-param-selector>
 			<li ng-repeat-end template-param-selector>
 			</li>
 			</li>
+			-->
 
 
 			<li class="tight-form-item" style="width: 15px">
 			<li class="tight-form-item" style="width: 15px">
 			</li>
 			</li>

+ 92 - 70
src/app/partials/templating_editor.html

@@ -37,7 +37,7 @@
 								{{variable.query}}
 								{{variable.query}}
 							</td>
 							</td>
 							<td style="width: 1%">
 							<td style="width: 1%">
-								<a ng-click="edit(variable)" class="btn btn-success btn-small">
+								<a ng-click="edit(variable)" class="btn btn-inverse btn-small">
 									<i class="fa fa-edit"></i>
 									<i class="fa fa-edit"></i>
 									Edit
 									Edit
 								</a>
 								</a>
@@ -56,97 +56,119 @@
 		</div>
 		</div>
 
 
 		<div ng-if="editor.index == 1 || (editor.index == 2 && !currentIsNew)">
 		<div ng-if="editor.index == 1 || (editor.index == 2 && !currentIsNew)">
-			<div class="editor-option">
-				<div class="editor-row">
-					<div class="editor-option">
-						<label class="small">Variable name</label>
-						<input type="text" class="input-medium" ng-model='current.name' placeholder="name" required></input>
-					</div>
-					<div class="editor-option">
-						<label class="small">Type</label>
-						<select class="input-medium" ng-model="current.type" ng-options="f for f in ['query', 'interval', 'custom']" ng-change="typeChanged()"></select>
-					</div>
-					<div class="editor-option" ng-show="current.type === 'query'">
-						<label class="small">Datasource</label>
-						<select class="input input-medium" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"></select>
-					</div>
-
-					<editor-opt-bool text="Refresh on load" show-if="current.type === 'query'"
-						tip="Check if you want values to be updated on dashboard load, will slow down dashboard load time"
-						model="current.refresh"></editor-opt-bool>
-				</div>
-
-				<div ng-show="current.type === 'interval'">
+			<div class="editor-row">
+				<div class="section">
+					<h5>General</h5>
 					<div class="editor-row">
 					<div class="editor-row">
 						<div class="editor-option">
 						<div class="editor-option">
-							<label class="small">Values</label>
-							<input type="text" class="input-xxlarge" ng-model='current.query' ng-blur="runQuery()" placeholder="name"></input>
+							<label class="small">Variable name</label>
+							<input type="text" class="input-medium" ng-model='current.name' placeholder="name" required></input>
+						</div>
+						<div class="editor-option">
+							<label class="small">Type</label>
+							<select class="input-small" ng-model="current.type" ng-options="f for f in ['query', 'interval', 'custom']" ng-change="typeChanged()"></select>
+						</div>
+						<div class="editor-option" ng-show="current.type === 'query'">
+							<label class="small">Datasource</label>
+							<select class="input input-medium" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"></select>
 						</div>
 						</div>
 					</div>
 					</div>
-					<div class="editor-row">
-						<editor-opt-bool text="Include auto interval" model="current.auto" change="runQuery()"></editor-opt-bool>
-						<div class="editor-option" ng-show="current.auto">
-							<label class="small">Auto interval steps <tip>How many steps, roughly, the interval is rounded and will not always match this count<tip></label>
-							<select class="input-mini" ng-model="current.auto_count" ng-options="f for f in [3,5,10,30,50,100,200]" ng-change="runQuery()"></select>
+
+					<div ng-show="current.type === 'interval'">
+						<div class="editor-row">
+							<div class="editor-option">
+								<label class="small">Values</label>
+								<input type="text" class="input-large" ng-model='current.query' ng-blur="runQuery()" placeholder="name"></input>
+							</div>
+							<editor-opt-bool text="Include auto interval" model="current.auto" change="runQuery()"></editor-opt-bool>
+							<div class="editor-option" ng-show="current.auto">
+								<label class="small">Auto interval steps <tip>How many steps, roughly, the interval is rounded and will not always match this count<tip></label>
+								<select class="input-mini" ng-model="current.auto_count" ng-options="f for f in [3,5,10,30,50,100,200]" ng-change="runQuery()"></select>
+							</div>
 						</div>
 						</div>
 					</div>
 					</div>
-				</div>
 
 
-				<div ng-show="current.type === 'custom'">
-					<div class="editor-row">
-						<div class="editor-option">
-							<label class="small">Values seperated by comma</label>
-							<input type="text" class="input-xxlarge" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue"></input>
+					<div ng-show="current.type === 'custom'">
+						<div class="editor-row">
+							<div class="editor-option">
+								<label class="small">Values seperated by comma</label>
+								<input type="text" class="input-xxlarge" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue"></input>
+							</div>
+						</div>
+					</div>
+
+					<div ng-show="current.type === 'query'">
+						<h5>Values Query</h5>
+						<div class="editor-row">
+							<div class="editor-option form-inline">
+								<label class="small">Variable values query</label>
+								<input type="text" class="input-xxlarge" ng-model='current.query' placeholder="apps.servers.*"></input>
+								<button class="btn btn-small btn-success" ng-click="runQuery()" bs-tooltip="'Execute query'" data-placement="right"><i class="fa fa-play"></i></button>
+							</div>
+						</div>
+
+						<div class="editor-row" style="margin: 15px 0">
+							<div class="editor-option form-inline">
+								<label class="small">regex (optional, if you want to extract part of a series name or metric node segment)</label>
+								<input type="text" class="input-xxlarge" ng-model='current.regex' placeholder="/.*-(.*)-.*/"></input>
+								<button class="btn btn-small btn-success" ng-click="runQuery()" bs-tooltip="'execute query'" data-placement="right"><i class="fa fa-play"></i></button>
+							</div>
+						</div>
+
+						<div class="editor-row" style="margin: 15px 0">
+							<editor-opt-bool text="Refresh on load" show-if="current.type === 'query'"
+								tip="Check if you want values to be updated on dashboard load, will slow down dashboard load time"
+								model="current.refresh"></editor-opt-bool>
+
+							<editor-opt-bool text="All option" model="current.includeAll" change="runQuery()"></editor-opt-bool>
+							<div class="editor-option" ng-show="current.includeAll">
+								<label class="small">All format</label>
+								<select class="input-medium" ng-model="current.allFormat" ng-change="runQuery()" ng-options="f for f in ['glob', 'wildcard', 'regex wildcard', 'regex values']"></select>
+							</div>
+							<div class="editor-option" ng-show="current.includeAll">
+								<label class="small">All value</label>
+								<input type="text" class="input-xlarge" ng-model='current.options[0].value'></input>
+							</div>
 						</div>
 						</div>
 					</div>
 					</div>
 				</div>
 				</div>
 
 
-				<div ng-show="current.type === 'query'">
-					<div class="editor-row">
-						<div class="editor-option form-inline">
-							<label class="small">Variable values query</label>
-							<input type="text" class="input-xxlarge" ng-model='current.query' placeholder="apps.servers.*"></input>
-							<button class="btn btn-small btn-success" ng-click="runQuery()" bs-tooltip="'Execute query'" data-placement="right"><i class="fa fa-play"></i></button>
+				<div class="section">
+					<div class="section">
+						<h5>Display options</h5>
+						<div class="editor-option">
+							<label class="small">Variable label</label>
+							<input type="text" class="input-medium" ng-model='current.label' placeholder=""></input>
 						</div>
 						</div>
+						<editor-opt-bool text="Hide Label" model="current.hideLabel"></editor-opt-bool>
 					</div>
 					</div>
 
 
-					<div class="editor-row" style="margin: 15px 0">
-						<div class="editor-option form-inline">
-							<label class="small">regex (optional, if you want to extract part of a series name or metric node segment)</label>
-							<input type="text" class="input-xxlarge" ng-model='current.regex' placeholder="/.*-(.*)-.*/"></input>
-							<button class="btn btn-small btn-success" ng-click="runQuery()" bs-tooltip="'execute query'" data-placement="right"><i class="fa fa-play"></i></button>
+					<div class="section">
+						<h5>Multi-value selection <tip>Enables multiple values to be selected at the same time</tip></h5>
+						<editor-opt-bool text="Enable" model="current.multi"></editor-opt-bool>
+						<div class="editor-option" ng-show="current.multi">
+							<label class="small">Multi value format</label>
+							<select class="input-medium" ng-model="current.multiFormat" ng-options="f for f in ['glob', 'regex values']"></select>
 						</div>
 						</div>
 					</div>
 					</div>
 
 
 					<div class="editor-row" style="margin: 15px 0">
 					<div class="editor-row" style="margin: 15px 0">
-						<editor-opt-bool text="All option" model="current.includeAll" change="runQuery()"></editor-opt-bool>
-						<div class="editor-option" ng-show="current.includeAll">
-							<label class="small">All format</label>
-							<select class="input-medium" ng-model="current.allFormat" ng-change="runQuery()" ng-options="f for f in ['glob', 'wildcard', 'regex wildcard', 'regex values']"></select>
+						<div class="editor-option" >
+							<label class="small">Variable values (shows max 20)</label>
+							<ul class="grafana-options-list">
+								<li ng-repeat="option in current.options | limitTo: 20">
+									{{option.text}}
+								</li>
+							</ul>
 						</div>
 						</div>
-						<div class="editor-option" ng-show="current.includeAll">
-							<label class="small">All value</label>
-							<input type="text" class="input-xlarge" ng-model='current.options[0].value'></input>
-						</div>
-					</div>
-				</div>
-			</div>
-			<div class="editor-option">
-				<div class="editor-row">
-					<div class="editor-option" >
-						<label class="small">Variable values (showing 20/{{current.options.length}})</label>
-						<ul class="grafana-options-list">
-							<li ng-repeat="option in current.options | limitTo: 20">
-								{{option.text}}
-							</li>
-						</ul>
 					</div>
 					</div>
 				</div>
 				</div>
 			</div>
 			</div>
-		</div>
 
 
-		<button type="button" class="btn btn-success" ng-show="editor.index === 2" ng-click="update();">Update</button>
-		<button type="button" class="btn btn-success" ng-show="editor.index === 1" ng-click="add();">Add</button>
+			<button type="button" class="btn btn-success pull-right" ng-show="editor.index === 2" ng-click="update();">Update</button>
+			<button type="button" class="btn btn-success pull-right" ng-show="editor.index === 1" ng-click="add();">Add</button>
+
+			<div class="clearfix"></div>
+		</div>
 	</div>
 	</div>
-</div>
 
 

+ 5 - 0
src/css/less/forms.less

@@ -11,6 +11,11 @@ input[type="checkbox"].cr1 {
   display: none;
   display: none;
 }
 }
 
 
+.editor-option label.cr1 {
+  display: inline-block;
+  margin: 5px 0 1px 0;
+}
+
 label.cr1 {
 label.cr1 {
   display: inline-block;
   display: inline-block;
   height: 19px;
   height: 19px;

+ 1 - 0
src/css/less/grafana.less

@@ -62,6 +62,7 @@
   .main-view-container {
   .main-view-container {
     overflow: hidden;
     overflow: hidden;
     height: 0;
     height: 0;
+    padding: 0;
     .row-control-inner {
     .row-control-inner {
       display: none;
       display: none;
     }
     }

+ 44 - 1
src/css/less/submenu.less

@@ -5,7 +5,7 @@
 }
 }
 
 
 .submenu-controls {
 .submenu-controls {
-  margin: 10px 10px 0 10px;
+  margin: 15px 0px 10px 13px;
 }
 }
 
 
 .annotation-disabled, .annotation-disabled a {
 .annotation-disabled, .annotation-disabled a {
@@ -18,3 +18,46 @@
   }
   }
 }
 }
 
 
+.variable-value-link {
+  font-size: 16px;
+  margin-right: 20px;
+}
+
+.variable-value-dropdown {
+  position: absolute;
+  top: 43px;
+  min-width: 200px;
+  height: 400px;
+  background: @grafanaPanelBackground;
+  box-shadow: 0px 0px 55px 0px black;
+  border: 1px solid @grafanaTargetFuncBackground;
+  z-index: 1000;
+  padding: 10px;
+
+  .variable-options-container {
+    height: 350px;
+    overflow: auto;
+    display: block;
+    line-height: 28px;
+  }
+
+  .variable-option {
+    display: block;
+    .fa {
+      font-size: 130%;
+      position: relative;
+      top: 2px;
+      padding-right: 6px;
+    }
+    .fa-check-square-o { display: none; }
+
+    &.selected {
+      .fa-square-o {
+        display: none;
+      }
+      .fa-check-square-o {
+        display: inline-block;
+      }
+    }
+  }
+}

+ 0 - 4
src/css/less/tightform.less

@@ -5,10 +5,6 @@
   background: @grafanaTargetBackground;
   background: @grafanaTargetBackground;
   width: 100%;
   width: 100%;
 
 
-  .dropdown {
-    padding: 0; margin: 0;
-  }
-
   &:last-child, &.last {
   &:last-child, &.last {
     border-bottom: 1px solid @grafanaTargetBorder;
     border-bottom: 1px solid @grafanaTargetBorder;
   }
   }

+ 1 - 1
src/css/less/variables.dark.less

@@ -28,7 +28,7 @@
 // grafana Variables
 // grafana Variables
 // -------------------------
 // -------------------------
 @grafanaPanelBackground: 	@grayDarker;
 @grafanaPanelBackground: 	@grayDarker;
-@grafanaPanelBorder: 		solid 1px @grayDark;
+@grafanaPanelBorder: 		  solid 1px @grayDark;
 @grafanaTriggerBorder:		solid 1px #555;
 @grafanaTriggerBorder:		solid 1px #555;
 
 
 // Graphite Target Editor
 // Graphite Target Editor

+ 3 - 3
src/test/specs/sharePanelCtrl-specs.js

@@ -42,7 +42,7 @@ define([
       it('should remove panel id when toPanel is false', function() {
       it('should remove panel id when toPanel is false', function() {
         ctx.$location.path('/test');
         ctx.$location.path('/test');
         ctx.scope.panel = { id: 22 };
         ctx.scope.panel = { id: 22 };
-        ctx.scope.toPanel = false;
+        ctx.scope.options = { toPanel: false, forCurrent: true };
         setTime({ from: 'now-1h', to: 'now' });
         setTime({ from: 'now-1h', to: 'now' });
 
 
         ctx.scope.buildUrl();
         ctx.scope.buildUrl();
@@ -52,8 +52,8 @@ define([
       it('should include template variables in url', function() {
       it('should include template variables in url', function() {
         ctx.$location.path('/test');
         ctx.$location.path('/test');
         ctx.scope.panel = { id: 22 };
         ctx.scope.panel = { id: 22 };
-        ctx.scope.includeTemplateVars = true;
-        ctx.scope.toPanel = false;
+        ctx.scope.options = { includeTemplateVars: true, toPanel: false, forCurrent: true };
+
         ctx.templateSrv.variables = [{ name: 'app', current: {text: 'mupp' }}, {name: 'server', current: {text: 'srv-01'}}];
         ctx.templateSrv.variables = [{ name: 'app', current: {text: 'mupp' }}, {name: 'server', current: {text: 'srv-01'}}];
         setTime({ from: 'now-1h', to: 'now' });
         setTime({ from: 'now-1h', to: 'now' });
 
 

+ 28 - 0
src/test/specs/templateSrv-specs.js

@@ -45,7 +45,35 @@ define([
       });
       });
     });
     });
 
 
+    describe('render variable to string values', function() {
+      it('single value should return value', function() {
+        var result = _templateSrv.renderVariableValue({current: {value: 'test'}});
+        expect(result).to.be('test');
+      });
+
+      it('multi value and glob format should render glob string', function() {
+        var result = _templateSrv.renderVariableValue({
+          multiFormat: 'glob',
+          current: {
+            value: ['test','test2'],
+          }
+        });
+        expect(result).to.be('{test,test2}');
+      });
+
+      it('multi value and regex format should render regex string', function() {
+        var result = _templateSrv.renderVariableValue({
+          multiFormat: 'regex values',
+          current: {
+            value: ['test','test2'],
+          }
+        });
+        expect(result).to.be('(test|test2)');
+      });
+
+    });
 
 
+>>>>>>> template_var_multi_select
     describe('can check if variable exists', function() {
     describe('can check if variable exists', function() {
       beforeEach(function() {
       beforeEach(function() {
         _templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]);
         _templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]);