Merge branch 'template_var_multi_select' into panel_repeat
Conflicts: src/app/features/dashboard/submenuCtrl.js src/test/specs/templateSrv-specs.js
This commit is contained in:
@@ -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)
|
||||
|
||||
Generated
+3
-3
@@ -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
@@ -1,11 +1,11 @@
|
||||
Macaron [](https://drone.io/github.com/Unknwon/macaron/latest) [](http://gocover.io/github.com/Unknwon/macaron)
|
||||
=======================
|
||||
|
||||

|
||||

|
||||
|
||||
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)
|
||||
- [](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.
|
||||
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
# bpool [](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
|
||||
}
|
||||
```
|
||||
-45
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
+23
-23
@@ -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 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
|
||||
if len(vals) > 1 && vals[0].Kind() == reflect.Int {
|
||||
res.WriteHeader(int(vals[0].Int()))
|
||||
responseVal = vals[1]
|
||||
} else if len(vals) > 0 {
|
||||
responseVal = vals[0]
|
||||
}
|
||||
if canDeref(responseVal) {
|
||||
responseVal = responseVal.Elem()
|
||||
}
|
||||
if isByteSlice(responseVal) {
|
||||
res.Write(responseVal.Bytes())
|
||||
} else {
|
||||
res.Write([]byte(responseVal.String()))
|
||||
}
|
||||
}
|
||||
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 canDeref(val reflect.Value) bool {
|
||||
return val.Kind() == reflect.Interface || val.Kind() == reflect.Ptr
|
||||
func defaultReturnHandler() ReturnHandler {
|
||||
return func(ctx *Context, vals []reflect.Value) {
|
||||
rv := ctx.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
|
||||
res := rv.Interface().(http.ResponseWriter)
|
||||
var respVal reflect.Value
|
||||
if len(vals) > 1 && vals[0].Kind() == reflect.Int {
|
||||
res.WriteHeader(int(vals[0].Int()))
|
||||
respVal = vals[1]
|
||||
} else if len(vals) > 0 {
|
||||
respVal = vals[0]
|
||||
}
|
||||
if canDeref(respVal) {
|
||||
respVal = respVal.Elem()
|
||||
}
|
||||
if isByteSlice(respVal) {
|
||||
res.Write(respVal.Bytes())
|
||||
} else {
|
||||
res.Write([]byte(respVal.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
@@ -1 +1,3 @@
|
||||
testdata/conf_out.ini
|
||||
testdata/conf_out.ini
|
||||
ini.sublime-project
|
||||
ini.sublime-workspace
|
||||
|
||||
+34
-2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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 ]+")
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -16,4 +16,5 @@ define([
|
||||
'./grafanaVersionCheck',
|
||||
'./dropdown.typeahead',
|
||||
'./topnav',
|
||||
'./giveFocus',
|
||||
], function () {});
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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 +
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' : '';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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-repeat-end> -->
|
||||
<!-- <a class="tight-form-item" tabindex="1" data-toggle="dropdown">{{variable.current.text}} <i class="fa fa-caret-down"></i></a> -->
|
||||
<!-- <div class="dropdown-menu variable-values-dropdown"> -->
|
||||
<!-- <input type="text" class="fluid-width"> -->
|
||||
<!-- <div class="variable-values-list"> -->
|
||||
<!-- <div class="variable-values-list-item" ng-repeat="option in variable.options"> -->
|
||||
<!-- <editor-checkbox text="{{option.text}}" model="asd" change="buildUrl()"></editor-checkbox> -->
|
||||
<!-- </div> -->
|
||||
<!-- </div> -->
|
||||
<!-- </div> -->
|
||||
<!-- </li> -->
|
||||
<!-- -->
|
||||
<!--
|
||||
<li ng-repeat-end template-param-selector>
|
||||
</li>
|
||||
-->
|
||||
|
||||
<li class="tight-form-item" style="width: 15px">
|
||||
</li>
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
</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>
|
||||
<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 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 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="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">
|
||||
<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="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>
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
.main-view-container {
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
padding: 0;
|
||||
.row-control-inner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,6 @@
|
||||
background: @grafanaTargetBackground;
|
||||
width: 100%;
|
||||
|
||||
.dropdown {
|
||||
padding: 0; margin: 0;
|
||||
}
|
||||
|
||||
&:last-child, &.last {
|
||||
border-bottom: 1px solid @grafanaTargetBorder;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
// grafana Variables
|
||||
// -------------------------
|
||||
@grafanaPanelBackground: @grayDarker;
|
||||
@grafanaPanelBorder: solid 1px @grayDark;
|
||||
@grafanaPanelBorder: solid 1px @grayDark;
|
||||
@grafanaTriggerBorder: solid 1px #555;
|
||||
|
||||
// Graphite Target Editor
|
||||
|
||||
@@ -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' });
|
||||
|
||||
|
||||
@@ -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' } }]);
|
||||
|
||||
Reference in New Issue
Block a user