Merge branch 'develop' into template_var_multi_select

This commit is contained in:
Torkel Ödegaard 2015-03-19 10:17:34 -04:00
commit 01148ac1b9
30 changed files with 587 additions and 255 deletions

View File

@ -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)

6
Godeps/Godeps.json generated
View File

@ -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"
}
]
}

View File

@ -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
@ -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

View File

@ -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
}
```

View File

@ -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
}

View File

@ -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

View File

@ -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!")))

View File

@ -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

View File

@ -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) {

View File

@ -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
}

View File

@ -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 {

View File

@ -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()))
}
}
}

View File

@ -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

View File

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

View File

@ -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.

View File

@ -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) 的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。

View File

@ -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)

View File

@ -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)

View File

@ -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:

View File

@ -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")
})
}

View File

@ -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)
},
},
))
}

View File

@ -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},

View File

@ -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);
});
}

View File

@ -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.leftScale" ng-options="v as k for (k, v) in scaleTypes" 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.rightScale" ng-options="v as k for (k, v) in scaleTypes" ng-change="render()"></select>
</li>
<li class="tight-form-item">
Label
</li>

View File

@ -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,
scale: scope.panel.grid.leftScale,
max: scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.leftMax,
};
@ -356,16 +359,59 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
if (_.findWhere(data, {yaxis: 2})) {
var secondY = _.clone(defaults);
secondY.index = 2,
secondY.scale = scope.panel.grid.rightScale;
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.scale !== 2) {
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 = 10000000000;
}
}
axis.ticks = [0, 1];
var tick = 1;
while (true) {
tick = tick*10;
axis.ticks.push(tick);
if (tick > max) {
break;
}
}
axis.transform = function(v) { return Math.log(v+0.1); };
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 +457,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' : '';

View File

@ -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) {

View File

@ -53,10 +53,12 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
y_formats : ['short', 'short'],
// grid options
grid : {
leftScale: 1,
leftMax: null,
rightMax: null,
leftMin: null,
rightMin: null,
rightScale: 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.scaleTypes = {'linear': 1, 'log (base 10)': 2};
$scope.hiddenSeries = {};
$scope.seriesList = [];
$scope.unitFormats = kbn.getUnitFormats();

View File

@ -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">

View File

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

View File

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