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

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 лет назад
Родитель
Сommit
c7b4041879
53 измененных файлов с 986 добавлено и 376 удалено
  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 #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 #452](https://github.com/grafana/grafana/issues/452).   Graph: Adds logarithmic scale option (log base 10)
 
 **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)

+ 3 - 3
Godeps/Godeps.json

@@ -11,7 +11,7 @@
 		},
 		{
 			"ImportPath": "github.com/Unknwon/macaron",
-			"Rev": "da7cbddc50b9d33e076fb1eabff13b55c3b85fc5"
+			"Rev": "93de4f3fad97bf246b838f828e2348f46f21f20a"
 		},
 		{
 			"ImportPath": "github.com/codegangsta/cli",
@@ -72,8 +72,8 @@
 		},
 		{
 			"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 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.
 
-##### Current version: 0.5.0
+##### Current version: 0.5.4
 
 ## Getting Started
 
@@ -41,7 +41,7 @@ func main() {
 - Handy dependency injection powered by [inject](https://github.com/codegangsta/inject).
 - Better router layer and less reflection make faster speed.
 
-## Middlewares 
+## Middlewares
 
 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 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
 - [YouGam](http://yougam.com): Online Forum
 - [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
 
 - [API Reference](https://gowalker.org/github.com/Unknwon/macaron)
 - [Documentation](http://macaron.gogs.io)
 - [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
 
@@ -88,4 +91,4 @@ There are already many [middlewares](https://github.com/macaron-contrib) to simp
 
 ## 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"
 	"net/http"
 	"net/url"
+	"os"
 	"path"
 	"path/filepath"
 	"reflect"
+	"strconv"
 	"strings"
 	"time"
 
@@ -182,6 +184,11 @@ func (ctx *Context) Query(name string) string {
 	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.
 func (ctx *Context) QueryStrings(name string) []string {
 	if ctx.Req.Form == nil {
@@ -210,9 +217,21 @@ func (ctx *Context) QueryInt64(name string) int64 {
 	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.
-// e.g. ctx.Params(":uid")
+// e.g. ctx.Params(":uid") or ctx.Params("uid")
 func (ctx *Context) Params(name string) string {
+	if len(name) == 0 {
+		return ""
+	}
+	if name[0] != '*' && name[0] != ':' {
+		name = ":" + name
+	}
 	return ctx.params[name]
 }
 
@@ -242,12 +261,20 @@ func (ctx *Context) ParamsInt64(name string) int64 {
 	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.
 func (ctx *Context) GetFile(name string) (multipart.File, *multipart.FileHeader, error) {
 	return ctx.Req.FormFile(name)
 }
 
 // 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{}) {
 	cookie := http.Cookie{}
 	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 v, ok := others[1].(string); ok && len(v) > 0 {
 			cookie.Path = v
 		}
-	} else {
-		cookie.Path = "/"
 	}
 
-	// default empty
 	if len(others) > 2 {
 		if v, ok := others[2].(string); ok && len(v) > 0 {
 			cookie.Domain = v
 		}
 	}
 
-	// default empty
 	if len(others) > 3 {
 		switch v := others[3].(type) {
 		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 v, ok := others[4].(bool); ok && v {
 			cookie.HttpOnly = true
@@ -322,6 +344,12 @@ func (ctx *Context) GetCookieInt64(name string) int64 {
 	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
 
 // 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
 }
 
+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.
 func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interface{}) {
 	modtime := time.Now()
@@ -377,14 +413,35 @@ func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interfa
 			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)
 }
 
+// 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.
 func (ctx *Context) ServeFile(file string, names ...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() {
-			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() {
 			m.Get("/query", func(ctx *Context) string {
 				var buf bytes.Buffer
-				buf.WriteString(ctx.Query("name") + " ")
+				buf.WriteString(ctx.QueryTrim("name") + " ")
 				buf.WriteString(ctx.QueryEscape("name") + " ")
 				buf.WriteString(com.ToStr(ctx.QueryInt("int")) + " ")
 				buf.WriteString(com.ToStr(ctx.QueryInt64("int64")) + " ")
+				buf.WriteString(com.ToStr(ctx.QueryFloat64("float64")) + " ")
 				return buf.String()
 			})
 			m.Get("/query2", func(ctx *Context) string {
@@ -115,10 +133,10 @@ func Test_Context(t *testing.T) {
 			})
 
 			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)
 			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()
 			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() {
-			m.Get("/:name/:int/:int64", func(ctx *Context) string {
+			m.Get("/:name/:int/:int64/:float64", func(ctx *Context) string {
 				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.ParamsEscape(":name") + " ")
 				buf.WriteString(com.ToStr(ctx.ParamsInt(":int")) + " ")
 				buf.WriteString(com.ToStr(ctx.ParamsInt64(":int64")) + " ")
+				buf.WriteString(com.ToStr(ctx.ParamsFloat64(":float64")) + " ")
 				return buf.String()
 			})
 
 			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)
 			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() {
@@ -158,26 +178,29 @@ func Test_Context(t *testing.T) {
 
 		Convey("Set and get cookie", func() {
 			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()
 			req, err := http.NewRequest("GET", "/set", nil)
 			So(err, ShouldBeNil)
 			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 {
 				ctx.GetCookie("404")
 				So(ctx.GetCookieInt("uid"), ShouldEqual, 1)
 				So(ctx.GetCookieInt64("uid"), ShouldEqual, 1)
+				So(ctx.GetCookieFloat64("balance"), ShouldEqual, 1.25)
 				return ctx.GetCookie("user")
 			})
 
 			resp = httptest.NewRecorder()
 			req, err = http.NewRequest("GET", "/get", nil)
 			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)
 			So(resp.Body.String(), ShouldEqual, "Unknwon")
 		})
@@ -231,6 +254,39 @@ func Test_Context(t *testing.T) {
 			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() {
 			m.Get("/content", func(ctx *Context) {
 				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 2014 Unknown
+// Copyright 2014 Unknwon
 //
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
 // not use this file except in compliance with the License. You may obtain

+ 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)
 	})
 
-	if !isWindows {
+	if ColorLog {
 		Convey("Color console output", t, func() {
 			m := Classic()
 			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"
 )
 
-const _VERSION = "0.5.0.0116"
+const _VERSION = "0.5.4.0318"
 
 func Version() string {
 	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.
 func Config() *ini.File {
 	if cfg == nil {
-		return &ini.File{}
+		return ini.Empty()
 	}
 	return cfg
 }

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

@@ -1,4 +1,5 @@
 // Copyright 2013 Martini Authors
+// Copyright 2013 oxtoacart
 // Copyright 2014 Unknwon
 //
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
@@ -32,10 +33,39 @@ import (
 	"time"
 
 	"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 (
 	ContentType    = "Content-Type"
 	ContentLength  = "Content-Length"
@@ -50,7 +80,7 @@ const (
 
 var (
 	// 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
 	helperFuncs = template.FuncMap{
@@ -392,8 +422,10 @@ func (r *TplRender) RW() http.ResponseWriter {
 }
 
 func (r *TplRender) JSON(status int, v interface{}) {
-	var result []byte
-	var err error
+	var (
+		result []byte
+		err    error
+	)
 	if r.Opt.IndentJSON {
 		result, err = json.MarshalIndent(v, "", "  ")
 	} else {

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

@@ -1,5 +1,5 @@
 // Copyright 2013 Martini Authors
-// Copyright 2014 Unknown
+// Copyright 2014 Unknwon
 //
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
 // not use this file except in compliance with the License. You may obtain
@@ -28,32 +28,32 @@ import (
 // that are passed into this function.
 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 {
 	return func(ctx *Context, vals []reflect.Value) {
 		rv := ctx.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
 		res := rv.Interface().(http.ResponseWriter)
-		var responseVal reflect.Value
+		var respVal reflect.Value
 		if len(vals) > 1 && vals[0].Kind() == reflect.Int {
 			res.WriteHeader(int(vals[0].Int()))
-			responseVal = vals[1]
+			respVal = vals[1]
 		} 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 {
-			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
 // 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")
 ```
 
+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.
 
 ```go
@@ -58,7 +64,7 @@ When you're pretty sure the section exists, following code could make your life
 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:
 
@@ -117,6 +123,9 @@ val := cfg.Section("").Key("key name").String()
 To get value with types:
 
 ```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("FLOAT64").Float64()
 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("INT").InInt(5, []int{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.
 
+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:
 
 ```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
 
 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")
 ```
 
+或者从一个空白的文件开始:
+
+```go
+cfg := ini.Empty()
+```
+
 当您在一开始无法决定需要加载哪些数据源时,仍可以使用 **Append()** 在需要的时候加载它们。
 
 ```go
@@ -53,7 +59,7 @@ section, err := cfg.GetSection("")
 section := cfg.Section("")
 ```
 
-如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会返回一个空的分区对象
+如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会自动创建并返回一个对应的分区对象给您
 
 创建一个分区:
 
@@ -112,6 +118,9 @@ val := cfg.Section("").Key("key name").String()
 获取其它类型的值:
 
 ```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("FLOAT64").Float64()
 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("INT").InInt(5, []int{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):
 
 ```go
@@ -290,6 +310,16 @@ func main() {
 }
 ```
 
+结构的字段怎么设置默认值呢?很简单,只要在映射之前对指定字段进行赋值就可以了。如果键未找到或者类型错误,该值不会发生改变。
+
+```go
+// ...
+p := &Person{
+	Name: "Joe",
+}
+// ...
+```
+
 #### 名称映射器(Name Mapper)
 
 为了节省您的时间并简化代码,本库支持类型为 [`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.
 	_DEPTH_VALUES = 99
 
-	_VERSION = "1.0.1"
+	_VERSION = "1.2.6"
 )
 
 func Version() string {
@@ -144,9 +144,24 @@ func (k *Key) String() string {
 	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.
 func (k *Key) Bool() (bool, error) {
-	return strconv.ParseBool(k.String())
+	return parseBool(k.String())
 }
 
 // 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)
 }
 
+// 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.
 func (k *Key) Strings(delim string) []string {
 	str := k.String()
@@ -440,7 +501,10 @@ func (s *Section) GetKey(name string) (*Key, error) {
 func (s *Section) Key(name string) *Key {
 	key, err := s.GetKey(name)
 	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
 }
@@ -555,6 +619,13 @@ func Load(source interface{}, others ...interface{}) (_ *File, err error) {
 	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.
 func (f *File) NewSection(name string) (*Section, error) {
 	if len(name) == 0 {
@@ -575,6 +646,16 @@ func (f *File) NewSection(name string) (*Section, error) {
 	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.
 func (f *File) GetSection(name string) (*Section, error) {
 	if len(name) == 0 {
@@ -597,7 +678,10 @@ func (f *File) GetSection(name string) (*Section, error) {
 func (f *File) Section(name string) *Section {
 	sec, err := f.GetSection(name)
 	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
 }
@@ -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.
 func (f *File) parse(reader io.Reader) error {
 	buf := bufio.NewReader(reader)
@@ -776,7 +868,6 @@ func (f *File) parse(reader io.Reader) error {
 				val = lineRight[qLen:] + "\n"
 				for {
 					next, err := buf.ReadString('\n')
-					val += next
 					if err != nil {
 						if err != io.EOF {
 							return err
@@ -785,9 +876,10 @@ func (f *File) parse(reader io.Reader) error {
 					}
 					pos = strings.LastIndex(next, valQuote)
 					if pos > -1 {
-						val = val[:len(val)-len(valQuote)-1]
+						val += next[:pos]
 						break
 					}
+					val += next
 					if isEnd {
 						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]
 			}
 		} else {
-			val = strings.TrimSpace(lineRight[0:])
+			val = strings.TrimSpace(cutComment(lineRight[0:]))
 		}
 
 		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
 # Bio can be written in multiple lines.
 [author]
-NAME = Unknwon
+NAME = Unknwon  # Succeeding comment
 E-MAIL = fake@localhost
 GITHUB = https://github.com/%(NAME)s
 BIO = """Gopher.
 Coding addict.
 Good man.
-"""
+"""  # Succeeding comment
 
 [package]
 CLONE_URL = https://%(IMPORT_PATH)s
@@ -62,6 +62,7 @@ UNUSED_KEY = should be deleted
 [types]
 STRING = str
 BOOL = true
+BOOL_FALSE = false
 FLOAT64 = 1.25
 INT = 10
 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 with empty data", func() {
-			cfg, err := Load([]byte(""))
-			So(err, ShouldBeNil)
-			So(cfg, ShouldNotBeNil)
+			So(Empty(), ShouldNotBeNil)
 		})
 
 		Convey("Load with multiple data sources", func() {
@@ -203,6 +202,10 @@ func Test_Values(t *testing.T) {
 			So(err, ShouldBeNil)
 			So(v1, ShouldBeTrue)
 
+			v1, err = sec.Key("BOOL_FALSE").Bool()
+			So(err, ShouldBeNil)
+			So(v1, ShouldBeFalse)
+
 			v2, err := sec.Key("FLOAT64").Float64()
 			So(err, ShouldBeNil)
 			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() {
 			sec := cfg.Section("array")
 			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() {
-			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() {
@@ -321,6 +348,14 @@ func Test_Values(t *testing.T) {
 			cfg.DeleteSection("")
 			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() {
@@ -340,6 +375,10 @@ func Test_Values(t *testing.T) {
 			So(s, ShouldBeNil)
 		})
 
+		Convey("Create new sections with empty name", func() {
+			So(cfg.NewSections(""), ShouldNotBeNil)
+		})
+
 		Convey("Get section that not exists", func() {
 			s, err := cfg.GetSection("404")
 			So(err, ShouldNotBeNil)

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

@@ -29,7 +29,7 @@ type NameMapper func(string) string
 var (
 	// AllCapsUnderscore converts to format ALL_CAPS_UNDERSCORE.
 	AllCapsUnderscore NameMapper = func(raw string) string {
-		newstr := make([]rune, 0, 10)
+		newstr := make([]rune, 0, len(raw))
 		for i, chr := range raw {
 			if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
 				if i > 0 {
@@ -42,7 +42,7 @@ var (
 	}
 	// TitleUnderscore converts to format title_underscore.
 	TitleUnderscore NameMapper = func(raw string) string {
-		newstr := make([]rune, 0, 10)
+		newstr := make([]rune, 0, len(raw))
 		for i, chr := range raw {
 			if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
 				if i > 0 {
@@ -75,32 +75,38 @@ func parseDelim(actual string) string {
 
 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 {
 	switch kind {
 	case reflect.String:
+		if len(key.String()) == 0 {
+			return nil
+		}
 		field.SetString(key.String())
 	case reflect.Bool:
 		boolVal, err := key.Bool()
 		if err != nil {
-			return err
+			return nil
 		}
 		field.SetBool(boolVal)
 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 		intVal, err := key.Int64()
 		if err != nil {
-			return err
+			return nil
 		}
 		field.SetInt(intVal)
 	case reflect.Float64:
 		floatVal, err := key.Float64()
 		if err != nil {
-			return err
+			return nil
 		}
 		field.SetFloat(floatVal)
 	case reflectTime:
 		timeVal, err := key.Time()
 		if err != nil {
-			return err
+			return nil
 		}
 		field.Set(reflect.ValueOf(timeVal))
 	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"`
 }
 
-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
 }
 
 const _INVALID_DATA_CONF_STRUCT = `
+Name = 
 Age = age
 Male = 123
 Money = money
@@ -154,12 +144,20 @@ func Test_Struct(t *testing.T) {
 		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
 
 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)
 

+ 4 - 0
pkg/cmd/web.go

@@ -11,6 +11,7 @@ import (
 	"path"
 	"path/filepath"
 	"strconv"
+	"time"
 
 	"github.com/Unknwon/macaron"
 	"github.com/codegangsta/cli"
@@ -68,6 +69,9 @@ func mapStatic(m *macaron.Macaron, dir string, prefix string) {
 		macaron.StaticOptions{
 			SkipLogging: true,
 			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")
 )
 
+// Dashboard model
 type Dashboard struct {
 	Id      int64
 	Slug    string
@@ -27,6 +28,7 @@ type Dashboard struct {
 	Data  map[string]interface{}
 }
 
+// NewDashboard creates a new dashboard
 func NewDashboard(title string) *Dashboard {
 	dash := &Dashboard{}
 	dash.Data = make(map[string]interface{})
@@ -36,6 +38,7 @@ func NewDashboard(title string) *Dashboard {
 	return dash
 }
 
+// GetTags turns the tags in data json into go string array
 func (dash *Dashboard) GetTags() []string {
 	jsonTags := dash.Data["tags"]
 	if jsonTags == nil {
@@ -50,6 +53,7 @@ func (dash *Dashboard) GetTags() []string {
 	return b
 }
 
+// GetDashboardModel turns the command into the savable model
 func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
 	dash := &Dashboard{}
 	dash.Data = cmd.Dashboard
@@ -63,15 +67,19 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
 		if dash.Data["version"] != nil {
 			dash.Version = int(dash.Data["version"].(float64))
 		}
+	} else {
+		dash.Data["version"] = 0
 	}
 
 	return dash
 }
 
+// GetString a
 func (dash *Dashboard) GetString(prop string) string {
 	return dash.Data[prop].(string)
 }
 
+// UpdateSlug updates the slug
 func (dash *Dashboard) UpdateSlug() {
 	title := strings.ToLower(dash.Data["title"].(string))
 	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: "org_id", Type: DB_BigInt, 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: "created", 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
 	}
 
+	sess.publishAfterCommit(&events.OrgCreated{
+		Timestamp: org.Created,
+		Id:        org.Id,
+		Name:      org.Name,
+	})
+
 	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() {
 
     function djb2(str) {

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

@@ -16,4 +16,5 @@ define([
   './grafanaVersionCheck',
   './dropdown.typeahead',
   './topnav',
+  './giveFocus',
 ], 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
     .module('grafana.directives')
-    .directive('editorCheckbox', function($compile) {
+    .directive('editorCheckbox', function($compile, $interpolate) {
       return {
         restrict: 'E',
         link: function(scope, elem, attrs) {
+          var text = $interpolate(attrs.text)(scope);
           var ngchange = attrs.change ? (' ng-change="' + attrs.change + '"') : '';
           var tip = attrs.tip ? (' <tip>' + attrs.tip + '</tip>') : '';
           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" ' +
                           '       ng-model="' + attrs.model + '"' + ngchange +

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

@@ -20,17 +20,17 @@
 		<br>
 		<div class="gf-form">
 			<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 class="gf-form" ng-if="panel">
 				<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 class="gf-form">
 				<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>
 

+ 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.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 = function() {
@@ -38,7 +36,7 @@ function (angular, _, require, config) {
       params.from = range.from;
       params.to = range.to;
 
-      if ($scope.includeTemplateVars) {
+      if ($scope.options.includeTemplateVars) {
         _.each(templateSrv.variables, function(variable) {
           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.to;
       }
 
-      if ($scope.toPanel) {
+      if ($scope.options.toPanel) {
         params.panelId = $scope.panel.id;
         params.fullscreen = true;
       } else {

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

@@ -26,8 +26,8 @@ function (angular, _) {
       $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);
         $rootScope.$broadcast('refresh');
       });

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

@@ -30,8 +30,8 @@ function (angular, $, config) {
         link: function(scope, elem) {
           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);
           });
         }

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

@@ -17,6 +17,8 @@ function (angular, _) {
       options: [],
       includeAll: false,
       allFormat: 'glob',
+      multi: false,
+      multiFormat: 'glob',
     };
 
     $scope.init = function() {
@@ -75,7 +77,7 @@ function (angular, _) {
       if ($scope.current.datasource === void 0) {
         $scope.current.datasource = null;
         $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) {
         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);
     };
 
+    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._grafanaVariables[name] = value;
     };

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

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

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

@@ -30,6 +30,12 @@
 					empty-to-null ng-model="panel.grid.leftMin"
 					ng-change="render()" ng-model-onblur>
 				</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">
 					Label
 				</li>
@@ -69,6 +75,12 @@
 					empty-to-null ng-model="panel.grid.rightMin"
 					ng-change="render()" ng-model-onblur>
 				</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">
 					Label
 				</li>

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

@@ -27,6 +27,7 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
         var dashboard = scope.dashboard;
         var data, annotations;
         var sortedSeries;
+        var graphHeight;
         var legendSideLastValue = null;
         scope.crosshairEmiter = false;
 
@@ -64,19 +65,19 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
 
         function setElementHeight() {
           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) {
-              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;
           } catch(e) { // IE throws errors sometimes
@@ -349,6 +350,8 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
             position: 'left',
             show: scope.panel['y-axis'],
             min: scope.panel.grid.leftMin,
+            index: 1,
+            logBase: scope.panel.grid.leftLogBase,
             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})) {
             var secondY = _.clone(defaults);
+            secondY.index = 2,
+            secondY.logBase = scope.panel.grid.rightLogBase;
             secondY.position = 'right';
             secondY.min = scope.panel.grid.rightMin;
             secondY.max = scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.rightMax;
             options.yaxes.push(secondY);
+
+            applyLogScale(options.yaxes[1], data);
             configureAxisMode(options.yaxes[1], scope.panel.y_formats[1]);
           }
 
+          applyLogScale(options.yaxes[0], data);
           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) {
           axis.tickFormatter = function(val, axis) {
             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';
 
           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) {
-          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' : '';

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

@@ -99,7 +99,7 @@ function ($) {
       var group, value, timestamp, hoverInfo, i, series, seriesHtml;
 
       if(dashboard.sharedCrosshair){
-        scope.appEvent('setCrosshair',  { pos: pos, scope: scope });
+        scope.appEvent('setCrosshair', { pos: pos, scope: scope });
       }
 
       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'],
       // grid options
       grid          : {
+        leftLogBase: 1,
         leftMax: null,
         rightMax: null,
         leftMin: null,
         rightMin: null,
+        rightLogBase: 1,
         threshold1: null,
         threshold2: null,
         threshold1Color: 'rgba(216, 200, 27, 0.27)',
@@ -95,7 +97,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
       // tooltip options
       tooltip       : {
         value_type: 'cumulative',
-        shared: false,
+        shared: true,
       },
       // time overrides
       timeFrom: null,
@@ -114,6 +116,8 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
     _.defaults($scope.panel.grid, _d.grid);
     _.defaults($scope.panel.legend, _d.legend);
 
+    $scope.logScales = {'linear': 1, 'log (base 10)': 10};
+
     $scope.hiddenSeries = {};
     $scope.seriesList = [];
     $scope.unitFormats = kbn.getUnitFormats();

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

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

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

@@ -2,7 +2,7 @@
 
 	<div class="search-field-wrapper">
 		<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()" />
 		</span>
 		<div class="search-switches">

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

@@ -1,19 +1,28 @@
 <div class="submenu-controls" ng-controller="SubmenuCtrl">
 	<div class="tight-form borderless">
 
-
 		<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 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>
+			-->
 
 			<li class="tight-form-item" style="width: 15px">
 			</li>

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

@@ -37,7 +37,7 @@
 								{{variable.query}}
 							</td>
 							<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>
 									Edit
 								</a>
@@ -56,97 +56,119 @@
 		</div>
 
 		<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-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 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 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 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>
+						<editor-opt-bool text="Hide Label" model="current.hideLabel"></editor-opt-bool>
 					</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 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 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>
 
-		<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>
 

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

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

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

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

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

@@ -5,7 +5,7 @@
 }
 
 .submenu-controls {
-  margin: 10px 10px 0 10px;
+  margin: 15px 0px 10px 13px;
 }
 
 .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;
   width: 100%;
 
-  .dropdown {
-    padding: 0; margin: 0;
-  }
-
   &:last-child, &.last {
     border-bottom: 1px solid @grafanaTargetBorder;
   }

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

@@ -28,7 +28,7 @@
 // grafana Variables
 // -------------------------
 @grafanaPanelBackground: 	@grayDarker;
-@grafanaPanelBorder: 		solid 1px @grayDark;
+@grafanaPanelBorder: 		  solid 1px @grayDark;
 @grafanaTriggerBorder:		solid 1px #555;
 
 // 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() {
         ctx.$location.path('/test');
         ctx.scope.panel = { id: 22 };
-        ctx.scope.toPanel = false;
+        ctx.scope.options = { toPanel: false, forCurrent: true };
         setTime({ from: 'now-1h', to: 'now' });
 
         ctx.scope.buildUrl();
@@ -52,8 +52,8 @@ define([
       it('should include template variables in url', function() {
         ctx.$location.path('/test');
         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'}}];
         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() {
       beforeEach(function() {
         _templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]);