diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index dd4dd0e9e56..b5cb1a460df 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -124,6 +124,11 @@ "Comment": "v1.0.0", "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f" }, + { + "ImportPath": "github.com/bmizerany/assert", + "Comment": "release.r60-6-ge17e998", + "Rev": "e17e99893cb6509f428e1728281c2ad60a6b31e3" + }, { "ImportPath": "github.com/bradfitz/gomemcache/memcache", "Comment": "release.r60-40-g72a6864", @@ -237,6 +242,15 @@ "ImportPath": "github.com/klauspost/crc32", "Rev": "6834731faf32e62a2dd809d99fb24d1e4ae5a92d" }, + { + "ImportPath": "github.com/kr/pretty", + "Comment": "go.weekly.2011-12-22-27-ge6ac2fc", + "Rev": "e6ac2fc51e89a3249e82157fa0bb7a18ef9dd5bb" + }, + { + "ImportPath": "github.com/kr/text", + "Rev": "bb797dc4fb8320488f47bf11de07a733d7233e1f" + }, { "ImportPath": "github.com/lib/pq", "Comment": "go1.0-cutoff-13-g19eeca3", diff --git a/Godeps/_workspace/src/github.com/bmizerany/assert/.gitignore b/Godeps/_workspace/src/github.com/bmizerany/assert/.gitignore new file mode 100644 index 00000000000..b6fadf4ebe7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/bmizerany/assert/.gitignore @@ -0,0 +1,7 @@ +_go_.* +_gotest_.* +_obj +_test +_testmain.go +*.out +*.[568] diff --git a/Godeps/_workspace/src/github.com/bmizerany/assert/README.md b/Godeps/_workspace/src/github.com/bmizerany/assert/README.md new file mode 100644 index 00000000000..8b6b6fc4fc6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/bmizerany/assert/README.md @@ -0,0 +1,45 @@ +# Assert (c) Blake Mizerany and Keith Rarick -- MIT LICENCE + +## Assertions for Go tests + +## Install + + $ go get github.com/bmizerany/assert + +## Use + +**point.go** + + package point + + type Point struct { + x, y int + } + +**point_test.go** + + + package point + + import ( + "testing" + "github.com/bmizerany/assert" + ) + + func TestAsserts(t *testing.T) { + p1 := Point{1, 1} + p2 := Point{2, 1} + + assert.Equal(t, p1, p2) + } + +**output** + $ go test + --- FAIL: TestAsserts (0.00 seconds) + assert.go:15: /Users/flavio.barbosa/dev/stewie/src/point_test.go:12 + assert.go:24: ! X: 1 != 2 + FAIL + +## Docs + + http://github.com/bmizerany/assert diff --git a/Godeps/_workspace/src/github.com/bmizerany/assert/assert.go b/Godeps/_workspace/src/github.com/bmizerany/assert/assert.go new file mode 100644 index 00000000000..7409f985e80 --- /dev/null +++ b/Godeps/_workspace/src/github.com/bmizerany/assert/assert.go @@ -0,0 +1,76 @@ +package assert +// Testing helpers for doozer. + +import ( + "github.com/kr/pretty" + "reflect" + "testing" + "runtime" + "fmt" +) + +func assert(t *testing.T, result bool, f func(), cd int) { + if !result { + _, file, line, _ := runtime.Caller(cd + 1) + t.Errorf("%s:%d", file, line) + f() + t.FailNow() + } +} + +func equal(t *testing.T, exp, got interface{}, cd int, args ...interface{}) { + fn := func() { + for _, desc := range pretty.Diff(exp, got) { + t.Error("!", desc) + } + if len(args) > 0 { + t.Error("!", " -", fmt.Sprint(args...)) + } + } + result := reflect.DeepEqual(exp, got) + assert(t, result, fn, cd+1) +} + +func tt(t *testing.T, result bool, cd int, args ...interface{}) { + fn := func() { + t.Errorf("! Failure") + if len(args) > 0 { + t.Error("!", " -", fmt.Sprint(args...)) + } + } + assert(t, result, fn, cd+1) +} + +func T(t *testing.T, result bool, args ...interface{}) { + tt(t, result, 1, args...) +} + +func Tf(t *testing.T, result bool, format string, args ...interface{}) { + tt(t, result, 1, fmt.Sprintf(format, args...)) +} + +func Equal(t *testing.T, exp, got interface{}, args ...interface{}) { + equal(t, exp, got, 1, args...) +} + +func Equalf(t *testing.T, exp, got interface{}, format string, args ...interface{}) { + equal(t, exp, got, 1, fmt.Sprintf(format, args...)) +} + +func NotEqual(t *testing.T, exp, got interface{}, args ...interface{}) { + fn := func() { + t.Errorf("! Unexpected: <%#v>", exp) + if len(args) > 0 { + t.Error("!", " -", fmt.Sprint(args...)) + } + } + result := !reflect.DeepEqual(exp, got) + assert(t, result, fn, 1) +} + +func Panic(t *testing.T, err interface{}, fn func()) { + defer func() { + equal(t, err, recover(), 3) + }() + fn() +} diff --git a/Godeps/_workspace/src/github.com/bmizerany/assert/assert_test.go b/Godeps/_workspace/src/github.com/bmizerany/assert/assert_test.go new file mode 100644 index 00000000000..162a590c62f --- /dev/null +++ b/Godeps/_workspace/src/github.com/bmizerany/assert/assert_test.go @@ -0,0 +1,15 @@ +package assert + +import ( + "testing" +) + +func TestLineNumbers(t *testing.T) { + Equal(t, "foo", "foo", "msg!") + //Equal(t, "foo", "bar", "this should blow up") +} + +func TestNotEqual(t *testing.T) { + NotEqual(t, "foo", "bar", "msg!") + //NotEqual(t, "foo", "foo", "this should blow up") +} diff --git a/Godeps/_workspace/src/github.com/bmizerany/assert/example/point.go b/Godeps/_workspace/src/github.com/bmizerany/assert/example/point.go new file mode 100644 index 00000000000..15789fe10f4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/bmizerany/assert/example/point.go @@ -0,0 +1,5 @@ +package point + +type Point struct { + X, Y int +} diff --git a/Godeps/_workspace/src/github.com/bmizerany/assert/example/point_test.go b/Godeps/_workspace/src/github.com/bmizerany/assert/example/point_test.go new file mode 100644 index 00000000000..34e791a43c9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/bmizerany/assert/example/point_test.go @@ -0,0 +1,13 @@ +package point + +import ( + "testing" + "assert" +) + +func TestAsserts(t *testing.T) { + p1 := Point{1, 1} + p2 := Point{2, 1} + + assert.Equal(t, p1, p2) +} diff --git a/Godeps/_workspace/src/github.com/gopherjs/gopherjs/js/js.go b/Godeps/_workspace/src/github.com/gopherjs/gopherjs/js/js.go deleted file mode 100644 index 5367d3d0fa7..00000000000 --- a/Godeps/_workspace/src/github.com/gopherjs/gopherjs/js/js.go +++ /dev/null @@ -1,168 +0,0 @@ -// Package js provides functions for interacting with native JavaScript APIs. Calls to these functions are treated specially by GopherJS and translated directly to their corresponding JavaScript syntax. -// -// Use MakeWrapper to expose methods to JavaScript. When passing values directly, the following type conversions are performed: -// -// | Go type | JavaScript type | Conversions back to interface{} | -// | --------------------- | --------------------- | ------------------------------- | -// | bool | Boolean | bool | -// | integers and floats | Number | float64 | -// | string | String | string | -// | []int8 | Int8Array | []int8 | -// | []int16 | Int16Array | []int16 | -// | []int32, []int | Int32Array | []int | -// | []uint8 | Uint8Array | []uint8 | -// | []uint16 | Uint16Array | []uint16 | -// | []uint32, []uint | Uint32Array | []uint | -// | []float32 | Float32Array | []float32 | -// | []float64 | Float64Array | []float64 | -// | all other slices | Array | []interface{} | -// | arrays | see slice type | see slice type | -// | functions | Function | func(...interface{}) *js.Object | -// | time.Time | Date | time.Time | -// | - | instanceof Node | *js.Object | -// | maps, structs | instanceof Object | map[string]interface{} | -// -// Additionally, for a struct containing a *js.Object field, only the content of the field will be passed to JavaScript and vice versa. -package js - -// Object is a container for a native JavaScript object. Calls to its methods are treated specially by GopherJS and translated directly to their JavaScript syntax. A nil pointer to Object is equal to JavaScript's "null". Object can not be used as a map key. -type Object struct{ object *Object } - -// Get returns the object's property with the given key. -func (o *Object) Get(key string) *Object { return o.object.Get(key) } - -// Set assigns the value to the object's property with the given key. -func (o *Object) Set(key string, value interface{}) { o.object.Set(key, value) } - -// Delete removes the object's property with the given key. -func (o *Object) Delete(key string) { o.object.Delete(key) } - -// Length returns the object's "length" property, converted to int. -func (o *Object) Length() int { return o.object.Length() } - -// Index returns the i'th element of an array. -func (o *Object) Index(i int) *Object { return o.object.Index(i) } - -// SetIndex sets the i'th element of an array. -func (o *Object) SetIndex(i int, value interface{}) { o.object.SetIndex(i, value) } - -// Call calls the object's method with the given name. -func (o *Object) Call(name string, args ...interface{}) *Object { return o.object.Call(name, args...) } - -// Invoke calls the object itself. This will fail if it is not a function. -func (o *Object) Invoke(args ...interface{}) *Object { return o.object.Invoke(args...) } - -// New creates a new instance of this type object. This will fail if it not a function (constructor). -func (o *Object) New(args ...interface{}) *Object { return o.object.New(args...) } - -// Bool returns the object converted to bool according to JavaScript type conversions. -func (o *Object) Bool() bool { return o.object.Bool() } - -// String returns the object converted to string according to JavaScript type conversions. -func (o *Object) String() string { return o.object.String() } - -// Int returns the object converted to int according to JavaScript type conversions (parseInt). -func (o *Object) Int() int { return o.object.Int() } - -// Int64 returns the object converted to int64 according to JavaScript type conversions (parseInt). -func (o *Object) Int64() int64 { return o.object.Int64() } - -// Uint64 returns the object converted to uint64 according to JavaScript type conversions (parseInt). -func (o *Object) Uint64() uint64 { return o.object.Uint64() } - -// Float returns the object converted to float64 according to JavaScript type conversions (parseFloat). -func (o *Object) Float() float64 { return o.object.Float() } - -// Interface returns the object converted to interface{}. See GopherJS' README for details. -func (o *Object) Interface() interface{} { return o.object.Interface() } - -// Unsafe returns the object as an uintptr, which can be converted via unsafe.Pointer. Not intended for public use. -func (o *Object) Unsafe() uintptr { return o.object.Unsafe() } - -// Error encapsulates JavaScript errors. Those are turned into a Go panic and may be recovered, giving an *Error that holds the JavaScript error object. -type Error struct { - *Object -} - -// Error returns the message of the encapsulated JavaScript error object. -func (err *Error) Error() string { - return "JavaScript error: " + err.Get("message").String() -} - -// Stack returns the stack property of the encapsulated JavaScript error object. -func (err *Error) Stack() string { - return err.Get("stack").String() -} - -// Global gives JavaScript's global object ("window" for browsers and "GLOBAL" for Node.js). -var Global *Object - -// Module gives the value of the "module" variable set by Node.js. Hint: Set a module export with 'js.Module.Get("exports").Set("exportName", ...)'. -var Module *Object - -// Undefined gives the JavaScript value "undefined". -var Undefined *Object - -// Debugger gets compiled to JavaScript's "debugger;" statement. -func Debugger() {} - -// InternalObject returns the internal JavaScript object that represents i. Not intended for public use. -func InternalObject(i interface{}) *Object { - return nil -} - -// MakeFunc wraps a function and gives access to the values of JavaScript's "this" and "arguments" keywords. -func MakeFunc(func(this *Object, arguments []*Object) interface{}) *Object { - return nil -} - -// Keys returns the keys of the given JavaScript object. -func Keys(o *Object) []string { - if o == nil || o == Undefined { - return nil - } - a := Global.Get("Object").Call("keys", o) - s := make([]string, a.Length()) - for i := 0; i < a.Length(); i++ { - s[i] = a.Index(i).String() - } - return s -} - -// MakeWrapper creates a JavaScript object which has wrappers for the exported methods of i. Use explicit getter and setter methods to expose struct fields to JavaScript. -func MakeWrapper(i interface{}) *Object { - v := InternalObject(i) - o := Global.Get("Object").New() - o.Set("__internal_object__", v) - methods := v.Get("constructor").Get("methods") - for i := 0; i < methods.Length(); i++ { - m := methods.Index(i) - if m.Get("pkg").String() != "" { // not exported - continue - } - o.Set(m.Get("name").String(), func(args ...*Object) *Object { - return Global.Call("$externalizeFunction", v.Get(m.Get("prop").String()), m.Get("typ"), true).Call("apply", v, args) - }) - } - return o -} - -// NewArrayBuffer creates a JavaScript ArrayBuffer from a byte slice. -func NewArrayBuffer(b []byte) *Object { - slice := InternalObject(b) - offset := slice.Get("$offset").Int() - length := slice.Get("$length").Int() - return slice.Get("$array").Get("buffer").Call("slice", offset, offset+length) -} - -// M is a simple map type. It is intended as a shorthand for JavaScript objects (before conversion). -type M map[string]interface{} - -// S is a simple slice type. It is intended as a shorthand for JavaScript arrays (before conversion). -type S []interface{} - -func init() { - // avoid dead code elimination - e := Error{} - _ = e -} diff --git a/Godeps/_workspace/src/github.com/kr/pretty/.gitignore b/Godeps/_workspace/src/github.com/kr/pretty/.gitignore new file mode 100644 index 00000000000..1f0a99f2f2b --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/.gitignore @@ -0,0 +1,4 @@ +[568].out +_go* +_test* +_obj diff --git a/Godeps/_workspace/src/github.com/kr/pretty/License b/Godeps/_workspace/src/github.com/kr/pretty/License new file mode 100644 index 00000000000..05c783ccf68 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/License @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright 2012 Keith Rarick + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/kr/pretty/Readme b/Godeps/_workspace/src/github.com/kr/pretty/Readme new file mode 100644 index 00000000000..c589fc622b3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/Readme @@ -0,0 +1,9 @@ +package pretty + + import "github.com/kr/pretty" + + Package pretty provides pretty-printing for Go values. + +Documentation + + http://godoc.org/github.com/kr/pretty diff --git a/Godeps/_workspace/src/github.com/kr/pretty/diff.go b/Godeps/_workspace/src/github.com/kr/pretty/diff.go new file mode 100644 index 00000000000..8fe8e2405a0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/diff.go @@ -0,0 +1,158 @@ +package pretty + +import ( + "fmt" + "io" + "reflect" +) + +type sbuf []string + +func (s *sbuf) Write(b []byte) (int, error) { + *s = append(*s, string(b)) + return len(b), nil +} + +// Diff returns a slice where each element describes +// a difference between a and b. +func Diff(a, b interface{}) (desc []string) { + Fdiff((*sbuf)(&desc), a, b) + return desc +} + +// Fdiff writes to w a description of the differences between a and b. +func Fdiff(w io.Writer, a, b interface{}) { + diffWriter{w: w}.diff(reflect.ValueOf(a), reflect.ValueOf(b)) +} + +type diffWriter struct { + w io.Writer + l string // label +} + +func (w diffWriter) printf(f string, a ...interface{}) { + var l string + if w.l != "" { + l = w.l + ": " + } + fmt.Fprintf(w.w, l+f, a...) +} + +func (w diffWriter) diff(av, bv reflect.Value) { + if !av.IsValid() && bv.IsValid() { + w.printf("nil != %#v", bv.Interface()) + return + } + if av.IsValid() && !bv.IsValid() { + w.printf("%#v != nil", av.Interface()) + return + } + if !av.IsValid() && !bv.IsValid() { + return + } + + at := av.Type() + bt := bv.Type() + if at != bt { + w.printf("%v != %v", at, bt) + return + } + + // numeric types, including bool + if at.Kind() < reflect.Array { + a, b := av.Interface(), bv.Interface() + if a != b { + w.printf("%#v != %#v", a, b) + } + return + } + + switch at.Kind() { + case reflect.String: + a, b := av.Interface(), bv.Interface() + if a != b { + w.printf("%q != %q", a, b) + } + case reflect.Ptr: + switch { + case av.IsNil() && !bv.IsNil(): + w.printf("nil != %v", bv.Interface()) + case !av.IsNil() && bv.IsNil(): + w.printf("%v != nil", av.Interface()) + case !av.IsNil() && !bv.IsNil(): + w.diff(av.Elem(), bv.Elem()) + } + case reflect.Struct: + for i := 0; i < av.NumField(); i++ { + w.relabel(at.Field(i).Name).diff(av.Field(i), bv.Field(i)) + } + case reflect.Slice: + lenA := av.Len() + lenB := bv.Len() + if lenA != lenB { + w.printf("%s[%d] != %s[%d]", av.Type(), lenA, bv.Type(), lenB) + break + } + for i := 0; i < lenA; i++ { + w.relabel(fmt.Sprintf("[%d]", i)).diff(av.Index(i), bv.Index(i)) + } + case reflect.Map: + ak, both, bk := keyDiff(av.MapKeys(), bv.MapKeys()) + for _, k := range ak { + w := w.relabel(fmt.Sprintf("[%#v]", k.Interface())) + w.printf("%q != (missing)", av.MapIndex(k)) + } + for _, k := range both { + w := w.relabel(fmt.Sprintf("[%#v]", k.Interface())) + w.diff(av.MapIndex(k), bv.MapIndex(k)) + } + for _, k := range bk { + w := w.relabel(fmt.Sprintf("[%#v]", k.Interface())) + w.printf("(missing) != %q", bv.MapIndex(k)) + } + case reflect.Interface: + w.diff(reflect.ValueOf(av.Interface()), reflect.ValueOf(bv.Interface())) + default: + if !reflect.DeepEqual(av.Interface(), bv.Interface()) { + w.printf("%# v != %# v", Formatter(av.Interface()), Formatter(bv.Interface())) + } + } +} + +func (d diffWriter) relabel(name string) (d1 diffWriter) { + d1 = d + if d.l != "" && name[0] != '[' { + d1.l += "." + } + d1.l += name + return d1 +} + +func keyDiff(a, b []reflect.Value) (ak, both, bk []reflect.Value) { + for _, av := range a { + inBoth := false + for _, bv := range b { + if reflect.DeepEqual(av.Interface(), bv.Interface()) { + inBoth = true + both = append(both, av) + break + } + } + if !inBoth { + ak = append(ak, av) + } + } + for _, bv := range b { + inBoth := false + for _, av := range a { + if reflect.DeepEqual(av.Interface(), bv.Interface()) { + inBoth = true + break + } + } + if !inBoth { + bk = append(bk, bv) + } + } + return +} diff --git a/Godeps/_workspace/src/github.com/kr/pretty/diff_test.go b/Godeps/_workspace/src/github.com/kr/pretty/diff_test.go new file mode 100644 index 00000000000..3c388f13ca7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/diff_test.go @@ -0,0 +1,74 @@ +package pretty + +import ( + "testing" +) + +type difftest struct { + a interface{} + b interface{} + exp []string +} + +type S struct { + A int + S *S + I interface{} + C []int +} + +var diffs = []difftest{ + {a: nil, b: nil}, + {a: S{A: 1}, b: S{A: 1}}, + + {0, "", []string{`int != string`}}, + {0, 1, []string{`0 != 1`}}, + {S{}, new(S), []string{`pretty.S != *pretty.S`}}, + {"a", "b", []string{`"a" != "b"`}}, + {S{}, S{A: 1}, []string{`A: 0 != 1`}}, + {new(S), &S{A: 1}, []string{`A: 0 != 1`}}, + {S{S: new(S)}, S{S: &S{A: 1}}, []string{`S.A: 0 != 1`}}, + {S{}, S{I: 0}, []string{`I: nil != 0`}}, + {S{I: 1}, S{I: "x"}, []string{`I: int != string`}}, + {S{}, S{C: []int{1}}, []string{`C: []int[0] != []int[1]`}}, + {S{C: []int{}}, S{C: []int{1}}, []string{`C: []int[0] != []int[1]`}}, + {S{C: []int{1, 2, 3}}, S{C: []int{1, 2, 4}}, []string{`C[2]: 3 != 4`}}, + {S{}, S{A: 1, S: new(S)}, []string{`A: 0 != 1`, `S: nil != &{0 []}`}}, +} + +func TestDiff(t *testing.T) { + for _, tt := range diffs { + got := Diff(tt.a, tt.b) + eq := len(got) == len(tt.exp) + if eq { + for i := range got { + eq = eq && got[i] == tt.exp[i] + } + } + if !eq { + t.Errorf("diffing % #v", tt.a) + t.Errorf("with % #v", tt.b) + diffdiff(t, got, tt.exp) + continue + } + } +} + +func diffdiff(t *testing.T, got, exp []string) { + minus(t, "unexpected:", got, exp) + minus(t, "missing:", exp, got) +} + +func minus(t *testing.T, s string, a, b []string) { + var i, j int + for i = 0; i < len(a); i++ { + for j = 0; j < len(b); j++ { + if a[i] == b[j] { + break + } + } + if j == len(b) { + t.Error(s, a[i]) + } + } +} diff --git a/Godeps/_workspace/src/github.com/kr/pretty/example_test.go b/Godeps/_workspace/src/github.com/kr/pretty/example_test.go new file mode 100644 index 00000000000..ecf40f3fcc6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/example_test.go @@ -0,0 +1,20 @@ +package pretty_test + +import ( + "fmt" + "github.com/kr/pretty" +) + +func Example() { + type myType struct { + a, b int + } + var x = []myType{{1, 2}, {3, 4}, {5, 6}} + fmt.Printf("%# v", pretty.Formatter(x)) + // output: + // []pretty_test.myType{ + // {a:1, b:2}, + // {a:3, b:4}, + // {a:5, b:6}, + // } +} diff --git a/Godeps/_workspace/src/github.com/kr/pretty/formatter.go b/Godeps/_workspace/src/github.com/kr/pretty/formatter.go new file mode 100644 index 00000000000..8dacda25fa8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/formatter.go @@ -0,0 +1,337 @@ +package pretty + +import ( + "fmt" + "io" + "reflect" + "strconv" + "text/tabwriter" + + "github.com/kr/text" +) + +const ( + limit = 50 +) + +type formatter struct { + x interface{} + force bool + quote bool +} + +// Formatter makes a wrapper, f, that will format x as go source with line +// breaks and tabs. Object f responds to the "%v" formatting verb when both the +// "#" and " " (space) flags are set, for example: +// +// fmt.Sprintf("%# v", Formatter(x)) +// +// If one of these two flags is not set, or any other verb is used, f will +// format x according to the usual rules of package fmt. +// In particular, if x satisfies fmt.Formatter, then x.Format will be called. +func Formatter(x interface{}) (f fmt.Formatter) { + return formatter{x: x, quote: true} +} + +func (fo formatter) String() string { + return fmt.Sprint(fo.x) // unwrap it +} + +func (fo formatter) passThrough(f fmt.State, c rune) { + s := "%" + for i := 0; i < 128; i++ { + if f.Flag(i) { + s += string(i) + } + } + if w, ok := f.Width(); ok { + s += fmt.Sprintf("%d", w) + } + if p, ok := f.Precision(); ok { + s += fmt.Sprintf(".%d", p) + } + s += string(c) + fmt.Fprintf(f, s, fo.x) +} + +func (fo formatter) Format(f fmt.State, c rune) { + if fo.force || c == 'v' && f.Flag('#') && f.Flag(' ') { + w := tabwriter.NewWriter(f, 4, 4, 1, ' ', 0) + p := &printer{tw: w, Writer: w, visited: make(map[visit]int)} + p.printValue(reflect.ValueOf(fo.x), true, fo.quote) + w.Flush() + return + } + fo.passThrough(f, c) +} + +type printer struct { + io.Writer + tw *tabwriter.Writer + visited map[visit]int + depth int +} + +func (p *printer) indent() *printer { + q := *p + q.tw = tabwriter.NewWriter(p.Writer, 4, 4, 1, ' ', 0) + q.Writer = text.NewIndentWriter(q.tw, []byte{'\t'}) + return &q +} + +func (p *printer) printInline(v reflect.Value, x interface{}, showType bool) { + if showType { + io.WriteString(p, v.Type().String()) + fmt.Fprintf(p, "(%#v)", x) + } else { + fmt.Fprintf(p, "%#v", x) + } +} + +// printValue must keep track of already-printed pointer values to avoid +// infinite recursion. +type visit struct { + v uintptr + typ reflect.Type +} + +func (p *printer) printValue(v reflect.Value, showType, quote bool) { + if p.depth > 10 { + io.WriteString(p, "!%v(DEPTH EXCEEDED)") + return + } + + switch v.Kind() { + case reflect.Bool: + p.printInline(v, v.Bool(), showType) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + p.printInline(v, v.Int(), showType) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + p.printInline(v, v.Uint(), showType) + case reflect.Float32, reflect.Float64: + p.printInline(v, v.Float(), showType) + case reflect.Complex64, reflect.Complex128: + fmt.Fprintf(p, "%#v", v.Complex()) + case reflect.String: + p.fmtString(v.String(), quote) + case reflect.Map: + t := v.Type() + if showType { + io.WriteString(p, t.String()) + } + writeByte(p, '{') + if nonzero(v) { + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + keys := v.MapKeys() + for i := 0; i < v.Len(); i++ { + showTypeInStruct := true + k := keys[i] + mv := v.MapIndex(k) + pp.printValue(k, false, true) + writeByte(pp, ':') + if expand { + writeByte(pp, '\t') + } + showTypeInStruct = t.Elem().Kind() == reflect.Interface + pp.printValue(mv, showTypeInStruct, true) + if expand { + io.WriteString(pp, ",\n") + } else if i < v.Len()-1 { + io.WriteString(pp, ", ") + } + } + if expand { + pp.tw.Flush() + } + } + writeByte(p, '}') + case reflect.Struct: + t := v.Type() + if v.CanAddr() { + addr := v.UnsafeAddr() + vis := visit{addr, t} + if vd, ok := p.visited[vis]; ok && vd < p.depth { + p.fmtString(t.String()+"{(CYCLIC REFERENCE)}", false) + break // don't print v again + } + p.visited[vis] = p.depth + } + + if showType { + io.WriteString(p, t.String()) + } + writeByte(p, '{') + if nonzero(v) { + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + for i := 0; i < v.NumField(); i++ { + showTypeInStruct := true + if f := t.Field(i); f.Name != "" { + io.WriteString(pp, f.Name) + writeByte(pp, ':') + if expand { + writeByte(pp, '\t') + } + showTypeInStruct = labelType(f.Type) + } + pp.printValue(getField(v, i), showTypeInStruct, true) + if expand { + io.WriteString(pp, ",\n") + } else if i < v.NumField()-1 { + io.WriteString(pp, ", ") + } + } + if expand { + pp.tw.Flush() + } + } + writeByte(p, '}') + case reflect.Interface: + switch e := v.Elem(); { + case e.Kind() == reflect.Invalid: + io.WriteString(p, "nil") + case e.IsValid(): + pp := *p + pp.depth++ + pp.printValue(e, showType, true) + default: + io.WriteString(p, v.Type().String()) + io.WriteString(p, "(nil)") + } + case reflect.Array, reflect.Slice: + t := v.Type() + if showType { + io.WriteString(p, t.String()) + } + if v.Kind() == reflect.Slice && v.IsNil() && showType { + io.WriteString(p, "(nil)") + break + } + if v.Kind() == reflect.Slice && v.IsNil() { + io.WriteString(p, "nil") + break + } + writeByte(p, '{') + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + for i := 0; i < v.Len(); i++ { + showTypeInSlice := t.Elem().Kind() == reflect.Interface + pp.printValue(v.Index(i), showTypeInSlice, true) + if expand { + io.WriteString(pp, ",\n") + } else if i < v.Len()-1 { + io.WriteString(pp, ", ") + } + } + if expand { + pp.tw.Flush() + } + writeByte(p, '}') + case reflect.Ptr: + e := v.Elem() + if !e.IsValid() { + writeByte(p, '(') + io.WriteString(p, v.Type().String()) + io.WriteString(p, ")(nil)") + } else { + pp := *p + pp.depth++ + writeByte(pp, '&') + pp.printValue(e, true, true) + } + case reflect.Chan: + x := v.Pointer() + if showType { + writeByte(p, '(') + io.WriteString(p, v.Type().String()) + fmt.Fprintf(p, ")(%#v)", x) + } else { + fmt.Fprintf(p, "%#v", x) + } + case reflect.Func: + io.WriteString(p, v.Type().String()) + io.WriteString(p, " {...}") + case reflect.UnsafePointer: + p.printInline(v, v.Pointer(), showType) + case reflect.Invalid: + io.WriteString(p, "nil") + } +} + +func canInline(t reflect.Type) bool { + switch t.Kind() { + case reflect.Map: + return !canExpand(t.Elem()) + case reflect.Struct: + for i := 0; i < t.NumField(); i++ { + if canExpand(t.Field(i).Type) { + return false + } + } + return true + case reflect.Interface: + return false + case reflect.Array, reflect.Slice: + return !canExpand(t.Elem()) + case reflect.Ptr: + return false + case reflect.Chan, reflect.Func, reflect.UnsafePointer: + return false + } + return true +} + +func canExpand(t reflect.Type) bool { + switch t.Kind() { + case reflect.Map, reflect.Struct, + reflect.Interface, reflect.Array, reflect.Slice, + reflect.Ptr: + return true + } + return false +} + +func labelType(t reflect.Type) bool { + switch t.Kind() { + case reflect.Interface, reflect.Struct: + return true + } + return false +} + +func (p *printer) fmtString(s string, quote bool) { + if quote { + s = strconv.Quote(s) + } + io.WriteString(p, s) +} + +func tryDeepEqual(a, b interface{}) bool { + defer func() { recover() }() + return reflect.DeepEqual(a, b) +} + +func writeByte(w io.Writer, b byte) { + w.Write([]byte{b}) +} + +func getField(v reflect.Value, i int) reflect.Value { + val := v.Field(i) + if val.Kind() == reflect.Interface && !val.IsNil() { + val = val.Elem() + } + return val +} diff --git a/Godeps/_workspace/src/github.com/kr/pretty/formatter_test.go b/Godeps/_workspace/src/github.com/kr/pretty/formatter_test.go new file mode 100644 index 00000000000..5f3204e8e87 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/formatter_test.go @@ -0,0 +1,261 @@ +package pretty + +import ( + "fmt" + "io" + "strings" + "testing" + "unsafe" +) + +type test struct { + v interface{} + s string +} + +type LongStructTypeName struct { + longFieldName interface{} + otherLongFieldName interface{} +} + +type SA struct { + t *T + v T +} + +type T struct { + x, y int +} + +type F int + +func (f F) Format(s fmt.State, c rune) { + fmt.Fprintf(s, "F(%d)", int(f)) +} + +var long = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +var gosyntax = []test{ + {nil, `nil`}, + {"", `""`}, + {"a", `"a"`}, + {1, "int(1)"}, + {1.0, "float64(1)"}, + {[]int(nil), "[]int(nil)"}, + {[0]int{}, "[0]int{}"}, + {complex(1, 0), "(1+0i)"}, + //{make(chan int), "(chan int)(0x1234)"}, + {unsafe.Pointer(uintptr(unsafe.Pointer(&long))), fmt.Sprintf("unsafe.Pointer(0x%02x)", uintptr(unsafe.Pointer(&long)))}, + {func(int) {}, "func(int) {...}"}, + {map[int]int{1: 1}, "map[int]int{1:1}"}, + {int32(1), "int32(1)"}, + {io.EOF, `&errors.errorString{s:"EOF"}`}, + {[]string{"a"}, `[]string{"a"}`}, + { + []string{long}, + `[]string{"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}`, + }, + {F(5), "pretty.F(5)"}, + { + SA{&T{1, 2}, T{3, 4}}, + `pretty.SA{ + t: &pretty.T{x:1, y:2}, + v: pretty.T{x:3, y:4}, +}`, + }, + { + map[int][]byte{1: {}}, + `map[int][]uint8{ + 1: {}, +}`, + }, + { + map[int]T{1: {}}, + `map[int]pretty.T{ + 1: {}, +}`, + }, + { + long, + `"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"`, + }, + { + LongStructTypeName{ + longFieldName: LongStructTypeName{}, + otherLongFieldName: long, + }, + `pretty.LongStructTypeName{ + longFieldName: pretty.LongStructTypeName{}, + otherLongFieldName: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", +}`, + }, + { + &LongStructTypeName{ + longFieldName: &LongStructTypeName{}, + otherLongFieldName: (*LongStructTypeName)(nil), + }, + `&pretty.LongStructTypeName{ + longFieldName: &pretty.LongStructTypeName{}, + otherLongFieldName: (*pretty.LongStructTypeName)(nil), +}`, + }, + { + []LongStructTypeName{ + {nil, nil}, + {3, 3}, + {long, nil}, + }, + `[]pretty.LongStructTypeName{ + {}, + { + longFieldName: int(3), + otherLongFieldName: int(3), + }, + { + longFieldName: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + otherLongFieldName: nil, + }, +}`, + }, + { + []interface{}{ + LongStructTypeName{nil, nil}, + []byte{1, 2, 3}, + T{3, 4}, + LongStructTypeName{long, nil}, + }, + `[]interface {}{ + pretty.LongStructTypeName{}, + []uint8{0x1, 0x2, 0x3}, + pretty.T{x:3, y:4}, + pretty.LongStructTypeName{ + longFieldName: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + otherLongFieldName: nil, + }, +}`, + }, +} + +func TestGoSyntax(t *testing.T) { + for _, tt := range gosyntax { + s := fmt.Sprintf("%# v", Formatter(tt.v)) + if tt.s != s { + t.Errorf("expected %q", tt.s) + t.Errorf("got %q", s) + t.Errorf("expraw\n%s", tt.s) + t.Errorf("gotraw\n%s", s) + } + } +} + +type I struct { + i int + R interface{} +} + +func (i *I) I() *I { return i.R.(*I) } + +func TestCycle(t *testing.T) { + type A struct{ *A } + v := &A{} + v.A = v + + // panics from stack overflow without cycle detection + t.Logf("Example cycle:\n%# v", Formatter(v)) + + p := &A{} + s := fmt.Sprintf("%# v", Formatter([]*A{p, p})) + if strings.Contains(s, "CYCLIC") { + t.Errorf("Repeated address detected as cyclic reference:\n%s", s) + } + + type R struct { + i int + *R + } + r := &R{ + i: 1, + R: &R{ + i: 2, + R: &R{ + i: 3, + }, + }, + } + r.R.R.R = r + t.Logf("Example longer cycle:\n%# v", Formatter(r)) + + r = &R{ + i: 1, + R: &R{ + i: 2, + R: &R{ + i: 3, + R: &R{ + i: 4, + R: &R{ + i: 5, + R: &R{ + i: 6, + R: &R{ + i: 7, + R: &R{ + i: 8, + R: &R{ + i: 9, + R: &R{ + i: 10, + R: &R{ + i: 11, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + // here be pirates + r.R.R.R.R.R.R.R.R.R.R.R = r + t.Logf("Example very long cycle:\n%# v", Formatter(r)) + + i := &I{ + i: 1, + R: &I{ + i: 2, + R: &I{ + i: 3, + R: &I{ + i: 4, + R: &I{ + i: 5, + R: &I{ + i: 6, + R: &I{ + i: 7, + R: &I{ + i: 8, + R: &I{ + i: 9, + R: &I{ + i: 10, + R: &I{ + i: 11, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + iv := i.I().I().I().I().I().I().I().I().I().I() + *iv = *i + t.Logf("Example long interface cycle:\n%# v", Formatter(i)) +} diff --git a/Godeps/_workspace/src/github.com/kr/pretty/pretty.go b/Godeps/_workspace/src/github.com/kr/pretty/pretty.go new file mode 100644 index 00000000000..d3df8686ce8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/pretty.go @@ -0,0 +1,98 @@ +// Package pretty provides pretty-printing for Go values. This is +// useful during debugging, to avoid wrapping long output lines in +// the terminal. +// +// It provides a function, Formatter, that can be used with any +// function that accepts a format string. It also provides +// convenience wrappers for functions in packages fmt and log. +package pretty + +import ( + "fmt" + "io" + "log" +) + +// Errorf is a convenience wrapper for fmt.Errorf. +// +// Calling Errorf(f, x, y) is equivalent to +// fmt.Errorf(f, Formatter(x), Formatter(y)). +func Errorf(format string, a ...interface{}) error { + return fmt.Errorf(format, wrap(a, false)...) +} + +// Fprintf is a convenience wrapper for fmt.Fprintf. +// +// Calling Fprintf(w, f, x, y) is equivalent to +// fmt.Fprintf(w, f, Formatter(x), Formatter(y)). +func Fprintf(w io.Writer, format string, a ...interface{}) (n int, error error) { + return fmt.Fprintf(w, format, wrap(a, false)...) +} + +// Log is a convenience wrapper for log.Printf. +// +// Calling Log(x, y) is equivalent to +// log.Print(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Log(a ...interface{}) { + log.Print(wrap(a, true)...) +} + +// Logf is a convenience wrapper for log.Printf. +// +// Calling Logf(f, x, y) is equivalent to +// log.Printf(f, Formatter(x), Formatter(y)). +func Logf(format string, a ...interface{}) { + log.Printf(format, wrap(a, false)...) +} + +// Logln is a convenience wrapper for log.Printf. +// +// Calling Logln(x, y) is equivalent to +// log.Println(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Logln(a ...interface{}) { + log.Println(wrap(a, true)...) +} + +// Print pretty-prints its operands and writes to standard output. +// +// Calling Print(x, y) is equivalent to +// fmt.Print(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Print(a ...interface{}) (n int, errno error) { + return fmt.Print(wrap(a, true)...) +} + +// Printf is a convenience wrapper for fmt.Printf. +// +// Calling Printf(f, x, y) is equivalent to +// fmt.Printf(f, Formatter(x), Formatter(y)). +func Printf(format string, a ...interface{}) (n int, errno error) { + return fmt.Printf(format, wrap(a, false)...) +} + +// Println pretty-prints its operands and writes to standard output. +// +// Calling Print(x, y) is equivalent to +// fmt.Println(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Println(a ...interface{}) (n int, errno error) { + return fmt.Println(wrap(a, true)...) +} + +// Sprintf is a convenience wrapper for fmt.Sprintf. +// +// Calling Sprintf(f, x, y) is equivalent to +// fmt.Sprintf(f, Formatter(x), Formatter(y)). +func Sprintf(format string, a ...interface{}) string { + return fmt.Sprintf(format, wrap(a, false)...) +} + +func wrap(a []interface{}, force bool) []interface{} { + w := make([]interface{}, len(a)) + for i, x := range a { + w[i] = formatter{x: x, force: force} + } + return w +} diff --git a/Godeps/_workspace/src/github.com/kr/pretty/zero.go b/Godeps/_workspace/src/github.com/kr/pretty/zero.go new file mode 100644 index 00000000000..abb5b6fc14c --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/zero.go @@ -0,0 +1,41 @@ +package pretty + +import ( + "reflect" +) + +func nonzero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Bool: + return v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() != 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() != 0 + case reflect.Float32, reflect.Float64: + return v.Float() != 0 + case reflect.Complex64, reflect.Complex128: + return v.Complex() != complex(0, 0) + case reflect.String: + return v.String() != "" + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + if nonzero(getField(v, i)) { + return true + } + } + return false + case reflect.Array: + for i := 0; i < v.Len(); i++ { + if nonzero(v.Index(i)) { + return true + } + } + return false + case reflect.Map, reflect.Interface, reflect.Slice, reflect.Ptr, reflect.Chan, reflect.Func: + return !v.IsNil() + case reflect.UnsafePointer: + return v.Pointer() != 0 + } + return true +} diff --git a/Godeps/_workspace/src/github.com/kr/text/License b/Godeps/_workspace/src/github.com/kr/text/License new file mode 100644 index 00000000000..480a3280599 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/License @@ -0,0 +1,19 @@ +Copyright 2012 Keith Rarick + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/kr/text/Readme b/Godeps/_workspace/src/github.com/kr/text/Readme new file mode 100644 index 00000000000..7e6e7c0687b --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/Readme @@ -0,0 +1,3 @@ +This is a Go package for manipulating paragraphs of text. + +See http://go.pkgdoc.org/github.com/kr/text for full documentation. diff --git a/Godeps/_workspace/src/github.com/kr/text/colwriter/Readme b/Godeps/_workspace/src/github.com/kr/text/colwriter/Readme new file mode 100644 index 00000000000..1c1f4e68393 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/colwriter/Readme @@ -0,0 +1,5 @@ +Package colwriter provides a write filter that formats +input lines in multiple columns. + +The package is a straightforward translation from +/src/cmd/draw/mc.c in Plan 9 from User Space. diff --git a/Godeps/_workspace/src/github.com/kr/text/colwriter/column.go b/Godeps/_workspace/src/github.com/kr/text/colwriter/column.go new file mode 100644 index 00000000000..7302ce9f7a8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/colwriter/column.go @@ -0,0 +1,147 @@ +// Package colwriter provides a write filter that formats +// input lines in multiple columns. +// +// The package is a straightforward translation from +// /src/cmd/draw/mc.c in Plan 9 from User Space. +package colwriter + +import ( + "bytes" + "io" + "unicode/utf8" +) + +const ( + tab = 4 +) + +const ( + // Print each input line ending in a colon ':' separately. + BreakOnColon uint = 1 << iota +) + +// A Writer is a filter that arranges input lines in as many columns as will +// fit in its width. Tab '\t' chars in the input are translated to sequences +// of spaces ending at multiples of 4 positions. +// +// If BreakOnColon is set, each input line ending in a colon ':' is written +// separately. +// +// The Writer assumes that all Unicode code points have the same width; this +// may not be true in some fonts. +type Writer struct { + w io.Writer + buf []byte + width int + flag uint +} + +// NewWriter allocates and initializes a new Writer writing to w. +// Parameter width controls the total number of characters on each line +// across all columns. +func NewWriter(w io.Writer, width int, flag uint) *Writer { + return &Writer{ + w: w, + width: width, + flag: flag, + } +} + +// Write writes p to the writer w. The only errors returned are ones +// encountered while writing to the underlying output stream. +func (w *Writer) Write(p []byte) (n int, err error) { + var linelen int + var lastWasColon bool + for i, c := range p { + w.buf = append(w.buf, c) + linelen++ + if c == '\t' { + w.buf[len(w.buf)-1] = ' ' + for linelen%tab != 0 { + w.buf = append(w.buf, ' ') + linelen++ + } + } + if w.flag&BreakOnColon != 0 && c == ':' { + lastWasColon = true + } else if lastWasColon { + if c == '\n' { + pos := bytes.LastIndex(w.buf[:len(w.buf)-1], []byte{'\n'}) + if pos < 0 { + pos = 0 + } + line := w.buf[pos:] + w.buf = w.buf[:pos] + if err = w.columnate(); err != nil { + if len(line) < i { + return i - len(line), err + } + return 0, err + } + if n, err := w.w.Write(line); err != nil { + if r := len(line) - n; r < i { + return i - r, err + } + return 0, err + } + } + lastWasColon = false + } + if c == '\n' { + linelen = 0 + } + } + return len(p), nil +} + +// Flush should be called after the last call to Write to ensure that any data +// buffered in the Writer is written to output. +func (w *Writer) Flush() error { + return w.columnate() +} + +func (w *Writer) columnate() error { + words := bytes.Split(w.buf, []byte{'\n'}) + w.buf = nil + if len(words[len(words)-1]) == 0 { + words = words[:len(words)-1] + } + maxwidth := 0 + for _, wd := range words { + if n := utf8.RuneCount(wd); n > maxwidth { + maxwidth = n + } + } + maxwidth++ // space char + wordsPerLine := w.width / maxwidth + if wordsPerLine <= 0 { + wordsPerLine = 1 + } + nlines := (len(words) + wordsPerLine - 1) / wordsPerLine + for i := 0; i < nlines; i++ { + col := 0 + endcol := 0 + for j := i; j < len(words); j += nlines { + endcol += maxwidth + _, err := w.w.Write(words[j]) + if err != nil { + return err + } + col += utf8.RuneCount(words[j]) + if j+nlines < len(words) { + for col < endcol { + _, err := w.w.Write([]byte{' '}) + if err != nil { + return err + } + col++ + } + } + } + _, err := w.w.Write([]byte{'\n'}) + if err != nil { + return err + } + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/kr/text/colwriter/column_test.go b/Godeps/_workspace/src/github.com/kr/text/colwriter/column_test.go new file mode 100644 index 00000000000..ce388f5a260 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/colwriter/column_test.go @@ -0,0 +1,90 @@ +package colwriter + +import ( + "bytes" + "testing" +) + +var src = ` +.git +.gitignore +.godir +Procfile: +README.md +api.go +apps.go +auth.go +darwin.go +data.go +dyno.go: +env.go +git.go +help.go +hkdist +linux.go +ls.go +main.go +plugin.go +run.go +scale.go +ssh.go +tail.go +term +unix.go +update.go +version.go +windows.go +`[1:] + +var tests = []struct { + wid int + flag uint + src string + want string +}{ + {80, 0, "", ""}, + {80, 0, src, ` +.git README.md darwin.go git.go ls.go scale.go unix.go +.gitignore api.go data.go help.go main.go ssh.go update.go +.godir apps.go dyno.go: hkdist plugin.go tail.go version.go +Procfile: auth.go env.go linux.go run.go term windows.go +`[1:]}, + {80, BreakOnColon, src, ` +.git .gitignore .godir + +Procfile: +README.md api.go apps.go auth.go darwin.go data.go + +dyno.go: +env.go hkdist main.go scale.go term version.go +git.go linux.go plugin.go ssh.go unix.go windows.go +help.go ls.go run.go tail.go update.go +`[1:]}, + {20, 0, ` +Hello +Γειά σου +안녕 +今日は +`[1:], ` +Hello 안녕 +Γειά σου 今日は +`[1:]}, +} + +func TestWriter(t *testing.T) { + for _, test := range tests { + b := new(bytes.Buffer) + w := NewWriter(b, test.wid, test.flag) + if _, err := w.Write([]byte(test.src)); err != nil { + t.Error(err) + } + if err := w.Flush(); err != nil { + t.Error(err) + } + if g := b.String(); test.want != g { + t.Log("\n" + test.want) + t.Log("\n" + g) + t.Errorf("%q != %q", test.want, g) + } + } +} diff --git a/Godeps/_workspace/src/github.com/kr/text/doc.go b/Godeps/_workspace/src/github.com/kr/text/doc.go new file mode 100644 index 00000000000..cf4c198f955 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/doc.go @@ -0,0 +1,3 @@ +// Package text provides rudimentary functions for manipulating text in +// paragraphs. +package text diff --git a/Godeps/_workspace/src/github.com/kr/text/indent.go b/Godeps/_workspace/src/github.com/kr/text/indent.go new file mode 100644 index 00000000000..4ebac45c092 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/indent.go @@ -0,0 +1,74 @@ +package text + +import ( + "io" +) + +// Indent inserts prefix at the beginning of each non-empty line of s. The +// end-of-line marker is NL. +func Indent(s, prefix string) string { + return string(IndentBytes([]byte(s), []byte(prefix))) +} + +// IndentBytes inserts prefix at the beginning of each non-empty line of b. +// The end-of-line marker is NL. +func IndentBytes(b, prefix []byte) []byte { + var res []byte + bol := true + for _, c := range b { + if bol && c != '\n' { + res = append(res, prefix...) + } + res = append(res, c) + bol = c == '\n' + } + return res +} + +// Writer indents each line of its input. +type indentWriter struct { + w io.Writer + bol bool + pre [][]byte + sel int + off int +} + +// NewIndentWriter makes a new write filter that indents the input +// lines. Each line is prefixed in order with the corresponding +// element of pre. If there are more lines than elements, the last +// element of pre is repeated for each subsequent line. +func NewIndentWriter(w io.Writer, pre ...[]byte) io.Writer { + return &indentWriter{ + w: w, + pre: pre, + bol: true, + } +} + +// The only errors returned are from the underlying indentWriter. +func (w *indentWriter) Write(p []byte) (n int, err error) { + for _, c := range p { + if w.bol { + var i int + i, err = w.w.Write(w.pre[w.sel][w.off:]) + w.off += i + if err != nil { + return n, err + } + } + _, err = w.w.Write([]byte{c}) + if err != nil { + return n, err + } + n++ + w.bol = c == '\n' + if w.bol { + w.off = 0 + if w.sel < len(w.pre)-1 { + w.sel++ + } + } + } + return n, nil +} diff --git a/Godeps/_workspace/src/github.com/kr/text/indent_test.go b/Godeps/_workspace/src/github.com/kr/text/indent_test.go new file mode 100644 index 00000000000..5c723eee855 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/indent_test.go @@ -0,0 +1,119 @@ +package text + +import ( + "bytes" + "testing" +) + +type T struct { + inp, exp, pre string +} + +var tests = []T{ + { + "The quick brown fox\njumps over the lazy\ndog.\nBut not quickly.\n", + "xxxThe quick brown fox\nxxxjumps over the lazy\nxxxdog.\nxxxBut not quickly.\n", + "xxx", + }, + { + "The quick brown fox\njumps over the lazy\ndog.\n\nBut not quickly.", + "xxxThe quick brown fox\nxxxjumps over the lazy\nxxxdog.\n\nxxxBut not quickly.", + "xxx", + }, +} + +func TestIndent(t *testing.T) { + for _, test := range tests { + got := Indent(test.inp, test.pre) + if got != test.exp { + t.Errorf("mismatch %q != %q", got, test.exp) + } + } +} + +type IndentWriterTest struct { + inp, exp string + pre []string +} + +var ts = []IndentWriterTest{ + { + ` +The quick brown fox +jumps over the lazy +dog. +But not quickly. +`[1:], + ` +xxxThe quick brown fox +xxxjumps over the lazy +xxxdog. +xxxBut not quickly. +`[1:], + []string{"xxx"}, + }, + { + ` +The quick brown fox +jumps over the lazy +dog. +But not quickly. +`[1:], + ` +xxaThe quick brown fox +xxxjumps over the lazy +xxxdog. +xxxBut not quickly. +`[1:], + []string{"xxa", "xxx"}, + }, + { + ` +The quick brown fox +jumps over the lazy +dog. +But not quickly. +`[1:], + ` +xxaThe quick brown fox +xxbjumps over the lazy +xxcdog. +xxxBut not quickly. +`[1:], + []string{"xxa", "xxb", "xxc", "xxx"}, + }, + { + ` +The quick brown fox +jumps over the lazy +dog. + +But not quickly.`[1:], + ` +xxaThe quick brown fox +xxxjumps over the lazy +xxxdog. +xxx +xxxBut not quickly.`[1:], + []string{"xxa", "xxx"}, + }, +} + +func TestIndentWriter(t *testing.T) { + for _, test := range ts { + b := new(bytes.Buffer) + pre := make([][]byte, len(test.pre)) + for i := range test.pre { + pre[i] = []byte(test.pre[i]) + } + w := NewIndentWriter(b, pre...) + if _, err := w.Write([]byte(test.inp)); err != nil { + t.Error(err) + } + if got := b.String(); got != test.exp { + t.Errorf("mismatch %q != %q", got, test.exp) + t.Log(got) + t.Log(test.exp) + } + } +} diff --git a/Godeps/_workspace/src/github.com/kr/text/mc/Readme b/Godeps/_workspace/src/github.com/kr/text/mc/Readme new file mode 100644 index 00000000000..519ddc00a13 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/mc/Readme @@ -0,0 +1,9 @@ +Command mc prints in multiple columns. + + Usage: mc [-] [-N] [file...] + +Mc splits the input into as many columns as will fit in N +print positions. If the output is a tty, the default N is +the number of characters in a terminal line; otherwise the +default N is 80. Under option - each input line ending in +a colon ':' is printed separately. diff --git a/Godeps/_workspace/src/github.com/kr/text/mc/mc.go b/Godeps/_workspace/src/github.com/kr/text/mc/mc.go new file mode 100644 index 00000000000..00169a30f16 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/mc/mc.go @@ -0,0 +1,62 @@ +// Command mc prints in multiple columns. +// +// Usage: mc [-] [-N] [file...] +// +// Mc splits the input into as many columns as will fit in N +// print positions. If the output is a tty, the default N is +// the number of characters in a terminal line; otherwise the +// default N is 80. Under option - each input line ending in +// a colon ':' is printed separately. +package main + +import ( + "github.com/kr/pty" + "github.com/kr/text/colwriter" + "io" + "log" + "os" + "strconv" +) + +func main() { + var width int + var flag uint + args := os.Args[1:] + for len(args) > 0 && len(args[0]) > 0 && args[0][0] == '-' { + if len(args[0]) > 1 { + width, _ = strconv.Atoi(args[0][1:]) + } else { + flag |= colwriter.BreakOnColon + } + args = args[1:] + } + if width < 1 { + _, width, _ = pty.Getsize(os.Stdout) + } + if width < 1 { + width = 80 + } + + w := colwriter.NewWriter(os.Stdout, width, flag) + if len(args) > 0 { + for _, s := range args { + if f, err := os.Open(s); err == nil { + copyin(w, f) + f.Close() + } else { + log.Println(err) + } + } + } else { + copyin(w, os.Stdin) + } +} + +func copyin(w *colwriter.Writer, r io.Reader) { + if _, err := io.Copy(w, r); err != nil { + log.Println(err) + } + if err := w.Flush(); err != nil { + log.Println(err) + } +} diff --git a/Godeps/_workspace/src/github.com/kr/text/wrap.go b/Godeps/_workspace/src/github.com/kr/text/wrap.go new file mode 100644 index 00000000000..b09bb03736d --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/wrap.go @@ -0,0 +1,86 @@ +package text + +import ( + "bytes" + "math" +) + +var ( + nl = []byte{'\n'} + sp = []byte{' '} +) + +const defaultPenalty = 1e5 + +// Wrap wraps s into a paragraph of lines of length lim, with minimal +// raggedness. +func Wrap(s string, lim int) string { + return string(WrapBytes([]byte(s), lim)) +} + +// WrapBytes wraps b into a paragraph of lines of length lim, with minimal +// raggedness. +func WrapBytes(b []byte, lim int) []byte { + words := bytes.Split(bytes.Replace(bytes.TrimSpace(b), nl, sp, -1), sp) + var lines [][]byte + for _, line := range WrapWords(words, 1, lim, defaultPenalty) { + lines = append(lines, bytes.Join(line, sp)) + } + return bytes.Join(lines, nl) +} + +// WrapWords is the low-level line-breaking algorithm, useful if you need more +// control over the details of the text wrapping process. For most uses, either +// Wrap or WrapBytes will be sufficient and more convenient. +// +// WrapWords splits a list of words into lines with minimal "raggedness", +// treating each byte as one unit, accounting for spc units between adjacent +// words on each line, and attempting to limit lines to lim units. Raggedness +// is the total error over all lines, where error is the square of the +// difference of the length of the line and lim. Too-long lines (which only +// happen when a single word is longer than lim units) have pen penalty units +// added to the error. +func WrapWords(words [][]byte, spc, lim, pen int) [][][]byte { + n := len(words) + + length := make([][]int, n) + for i := 0; i < n; i++ { + length[i] = make([]int, n) + length[i][i] = len(words[i]) + for j := i + 1; j < n; j++ { + length[i][j] = length[i][j-1] + spc + len(words[j]) + } + } + + nbrk := make([]int, n) + cost := make([]int, n) + for i := range cost { + cost[i] = math.MaxInt32 + } + for i := n - 1; i >= 0; i-- { + if length[i][n-1] <= lim || i == n-1 { + cost[i] = 0 + nbrk[i] = n + } else { + for j := i + 1; j < n; j++ { + d := lim - length[i][j-1] + c := d*d + cost[j] + if length[i][j-1] > lim { + c += pen // too-long lines get a worse penalty + } + if c < cost[i] { + cost[i] = c + nbrk[i] = j + } + } + } + } + + var lines [][][]byte + i := 0 + for i < n { + lines = append(lines, words[i:nbrk[i]]) + i = nbrk[i] + } + return lines +} diff --git a/Godeps/_workspace/src/github.com/kr/text/wrap_test.go b/Godeps/_workspace/src/github.com/kr/text/wrap_test.go new file mode 100644 index 00000000000..634b6e8ebb9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/wrap_test.go @@ -0,0 +1,62 @@ +package text + +import ( + "bytes" + "testing" +) + +var text = "The quick brown fox jumps over the lazy dog." + +func TestWrap(t *testing.T) { + exp := [][]string{ + {"The", "quick", "brown", "fox"}, + {"jumps", "over", "the", "lazy", "dog."}, + } + words := bytes.Split([]byte(text), sp) + got := WrapWords(words, 1, 24, defaultPenalty) + if len(exp) != len(got) { + t.Fail() + } + for i := range exp { + if len(exp[i]) != len(got[i]) { + t.Fail() + } + for j := range exp[i] { + if exp[i][j] != string(got[i][j]) { + t.Fatal(i, exp[i][j], got[i][j]) + } + } + } +} + +func TestWrapNarrow(t *testing.T) { + exp := "The\nquick\nbrown\nfox\njumps\nover\nthe\nlazy\ndog." + if Wrap(text, 5) != exp { + t.Fail() + } +} + +func TestWrapOneLine(t *testing.T) { + exp := "The quick brown fox jumps over the lazy dog." + if Wrap(text, 500) != exp { + t.Fail() + } +} + +func TestWrapBug1(t *testing.T) { + cases := []struct { + limit int + text string + want string + }{ + {4, "aaaaa", "aaaaa"}, + {4, "a aaaaa", "a\naaaaa"}, + } + + for _, test := range cases { + got := Wrap(test.text, test.limit) + if got != test.want { + t.Errorf("Wrap(%q, %d) = %q want %q", test.text, test.limit, got, test.want) + } + } +} diff --git a/docs/Makefile b/docs/Makefile index f24a63065b3..b1a72adc3f8 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -44,7 +44,7 @@ docs-test: docs-build $(DOCKER_RUN_DOCS) "$(DOCKER_DOCS_IMAGE)" ./test.sh docs-build: - git fetch https://github.com/grafana/grafana.git docs-2.5 && git diff --name-status FETCH_HEAD...HEAD -- . > changed-files + git fetch https://github.com/grafana/grafana.git docs-2.6 && git diff --name-status FETCH_HEAD...HEAD -- . > changed-files echo "$(GIT_BRANCH)" > GIT_BRANCH echo "$(GITCOMMIT)" > GITCOMMIT docker build -t "$(DOCKER_DOCS_IMAGE)" . diff --git a/docs/VERSION b/docs/VERSION index e70b4523ae7..4a36342fcab 100644 --- a/docs/VERSION +++ b/docs/VERSION @@ -1 +1 @@ -2.6.0 +3.0.0 diff --git a/docs/sources/plugins/installation.md b/docs/sources/plugins/installation.md index b6db0e82b2c..d3a8013ce2b 100644 --- a/docs/sources/plugins/installation.md +++ b/docs/sources/plugins/installation.md @@ -8,16 +8,16 @@ page_keywords: grafana, plugins, documentation ## Installing plugins -The easiest way to install plugins is by using the CLI tool grafana-cli which is bundled with grafana. Before any modification take place after modifieng plugins, grafana-server need to be restarted. +The easiest way to install plugins is by using the CLI tool grafana-cli which is bundled with grafana. Before any modification take place after modifying plugins, grafana-server needs to be restarted. ### Grafana plugin directory -On Linux systems the grafana-cli will assume that the grafana plugin directory is "/var/lib/grafana/plugins". Its possible to override the directory which grafana-cli will operate on by specifing the --path flag. On Windows systems this parameter have to be specified for every call. +On Linux systems the grafana-cli will assume that the grafana plugin directory is "/var/lib/grafana/plugins". It's possible to override the directory which grafana-cli will operate on by specifing the --path flag. On Windows systems this parameter have to be specified for every call. ### Grafana-cli commands List available plugins ``` -grafana-cli list-remove +grafana-cli list-remote ``` Install a plugin type diff --git a/pkg/api/api.go b/pkg/api/api.go index d4bb22149f8..30bf617e1b6 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -175,9 +175,8 @@ func Register(r *macaron.Macaron) { r.Group("/plugins", func() { r.Get("/", wrap(GetPluginList)) - r.Get("/dashboards/:pluginId", wrap(GetPluginDashboards)) - r.Post("/dashboards/install", bind(dtos.InstallPluginDashboardCmd{}), wrap(InstallPluginDashboard)) - + r.Get("/:pluginId/readme", wrap(GetPluginReadme)) + r.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards)) r.Get("/:pluginId/settings", wrap(GetPluginSettingById)) r.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting)) }, reqOrgAdmin) @@ -193,6 +192,7 @@ func Register(r *macaron.Macaron) { r.Get("/file/:file", GetDashboardFromJsonFile) r.Get("/home", GetHomeDashboard) r.Get("/tags", GetDashboardTags) + r.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard)) }) // Dashboard snapshots diff --git a/pkg/api/cloudwatch/metrics.go b/pkg/api/cloudwatch/metrics.go index 54ab565bede..f5e5274a202 100644 --- a/pkg/api/cloudwatch/metrics.go +++ b/pkg/api/cloudwatch/metrics.go @@ -130,11 +130,14 @@ func handleGetNamespaces(req *cwRequest, c *middleware.Context) { for key := range metricsMap { keys = append(keys, key) } - if customMetricsNamespaces, ok := req.DataSource.JsonData["customMetricsNamespaces"].(string); ok { - for _, key := range strings.Split(customMetricsNamespaces, ",") { + + customNamespaces := req.DataSource.JsonData.Get("customMetricsNamespaces").MustString() + if customNamespaces != "" { + for _, key := range strings.Split(customNamespaces, ",") { keys = append(keys, key) } } + sort.Sort(sort.StringSlice(keys)) result := []interface{}{} diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 8ed848a7b11..3c369ca5b7f 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -53,7 +53,6 @@ func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapsho } func GetDashboardSnapshot(c *middleware.Context) { - key := c.Params(":key") query := &m.GetDashboardSnapshotQuery{Key: key} @@ -136,5 +135,4 @@ func SearchDashboardSnapshots(c *middleware.Context) Response { } return Json(200, dtos) - //return Json(200, searchQuery.Result) } diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 36e51fdc28a..26295dd3d3c 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) @@ -52,26 +53,26 @@ type DashboardMeta struct { } type DashboardFullWithMeta struct { - Meta DashboardMeta `json:"meta"` - Dashboard map[string]interface{} `json:"dashboard"` + Meta DashboardMeta `json:"meta"` + Dashboard *simplejson.Json `json:"dashboard"` } type DataSource struct { - Id int64 `json:"id"` - OrgId int64 `json:"orgId"` - Name string `json:"name"` - Type string `json:"type"` - Access m.DsAccess `json:"access"` - Url string `json:"url"` - Password string `json:"password"` - User string `json:"user"` - Database string `json:"database"` - BasicAuth bool `json:"basicAuth"` - BasicAuthUser string `json:"basicAuthUser"` - BasicAuthPassword string `json:"basicAuthPassword"` - WithCredentials bool `json:"withCredentials"` - IsDefault bool `json:"isDefault"` - JsonData map[string]interface{} `json:"jsonData,omitempty"` + Id int64 `json:"id"` + OrgId int64 `json:"orgId"` + Name string `json:"name"` + Type string `json:"type"` + Access m.DsAccess `json:"access"` + Url string `json:"url"` + Password string `json:"password"` + User string `json:"user"` + Database string `json:"database"` + BasicAuth bool `json:"basicAuth"` + BasicAuthUser string `json:"basicAuthUser"` + BasicAuthPassword string `json:"basicAuthPassword"` + WithCredentials bool `json:"withCredentials"` + IsDefault bool `json:"isDefault"` + JsonData *simplejson.Json `json:"jsonData,omitempty"` } type MetricQueryResultDto struct { diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index 5f3dac76296..88683155006 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -26,9 +26,23 @@ type PluginListItem struct { Info *plugins.PluginInfo `json:"info"` } -type InstallPluginDashboardCmd struct { - PluginId string `json:"pluginId"` - Path string `json:"path"` - Reinstall bool `json:"reinstall"` - Inputs map[string]interface{} `json:"inputs"` +type PluginList []PluginListItem + +func (slice PluginList) Len() int { + return len(slice) +} + +func (slice PluginList) Less(i, j int) bool { + return slice[i].Name < slice[j].Name +} + +func (slice PluginList) Swap(i, j int) { + slice[i], slice[j] = slice[j], slice[i] +} + +type ImportDashboardCommand struct { + PluginId string `json:"pluginId"` + Path string `json:"path"` + Reinstall bool `json:"reinstall"` + Inputs []plugins.ImportDashboardInput `json:"inputs"` } diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 8c86f98220a..c84d7faccff 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -59,7 +59,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro defaultDatasource = ds.Name } - if len(ds.JsonData) > 0 { + if len(ds.JsonData.MustMap()) > 0 { dsMap["jsonData"] = ds.JsonData } diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 1ba0dde6c9f..56d0d99296c 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -1,6 +1,8 @@ package api import ( + "sort" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" @@ -19,7 +21,7 @@ func GetPluginList(c *middleware.Context) Response { return ApiError(500, "Failed to get list of plugins", err) } - result := make([]*dtos.PluginListItem, 0) + result := make(dtos.PluginList, 0) for _, pluginDef := range plugins.Plugins { // filter out app sub plugins if embeddedFilter == "0" && pluginDef.IncludedInAppId != "" { @@ -31,7 +33,7 @@ func GetPluginList(c *middleware.Context) Response { continue } - listItem := &dtos.PluginListItem{ + listItem := dtos.PluginListItem{ Id: pluginDef.Id, Name: pluginDef.Name, Type: pluginDef.Type, @@ -48,9 +50,17 @@ func GetPluginList(c *middleware.Context) Response { continue } + // filter out built in data sources + if ds, exists := plugins.DataSources[pluginDef.Id]; exists { + if ds.BuiltIn { + continue + } + } + result = append(result, listItem) } + sort.Sort(result) return Json(200, result) } @@ -122,9 +132,23 @@ func GetPluginDashboards(c *middleware.Context) Response { } } -func InstallPluginDashboard(c *middleware.Context, apiCmd dtos.InstallPluginDashboardCmd) Response { +func GetPluginReadme(c *middleware.Context) Response { + pluginId := c.Params(":pluginId") - cmd := plugins.InstallPluginDashboardCommand{ + if content, err := plugins.GetPluginReadme(pluginId); err != nil { + if notfound, ok := err.(plugins.PluginNotFoundError); ok { + return ApiError(404, notfound.Error(), nil) + } + + return ApiError(500, "Could not get readme", err) + } else { + return Respond(200, content) + } +} + +func ImportDashboard(c *middleware.Context, apiCmd dtos.ImportDashboardCommand) Response { + + cmd := plugins.ImportDashboardCommand{ OrgId: c.OrgId, UserId: c.UserId, PluginId: apiCmd.PluginId, diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index 7ed93e825d3..57c0c324320 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -15,6 +15,7 @@ import ( "os" "path" "regexp" + "strings" ) func validateInput(c CommandLine, pluginFolder string) error { @@ -156,7 +157,11 @@ func downloadFile(pluginName, filePath, url string) (err error) { } else { dst, err := os.Create(newFile) if err != nil { - log.Errorf("%v", err) + if strings.Contains(err.Error(), "permission denied") { + return fmt.Errorf( + "Could not create file %s. permission deined. Make sure you have write access to plugindir", + newFile) + } } defer dst.Close() src, err := zf.Open() diff --git a/pkg/components/dynmap/dynmap.go b/pkg/components/dynmap/dynmap.go new file mode 100644 index 00000000000..797694845cd --- /dev/null +++ b/pkg/components/dynmap/dynmap.go @@ -0,0 +1,817 @@ +// uses code from https://github.com/antonholmquist/jason/blob/master/jason.go +// MIT Licence + +package dynmap + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "strings" +) + +// Error values returned when validation functions fail +var ( + ErrNotNull = errors.New("is not null") + ErrNotArray = errors.New("Not an array") + ErrNotNumber = errors.New("not a number") + ErrNotBool = errors.New("no bool") + ErrNotObject = errors.New("not an object") + ErrNotObjectArray = errors.New("not an object array") + ErrNotString = errors.New("not a string") +) + +type KeyNotFoundError struct { + Key string +} + +func (k KeyNotFoundError) Error() string { + if k.Key != "" { + return fmt.Sprintf("key '%s' not found", k.Key) + } + + return "key not found" +} + +// Value represents an arbitrary JSON value. +// It may contain a bool, number, string, object, array or null. +type Value struct { + data interface{} + exists bool // Used to separate nil and non-existing values +} + +// Object represents an object JSON object. +// It inherets from Value but with an additional method to access +// a map representation of it's content. It's useful when iterating. +type Object struct { + Value + m map[string]*Value + valid bool +} + +// Returns the golang map. +// Needed when iterating through the values of the object. +func (v *Object) Map() map[string]*Value { + return v.m +} + +func NewFromMap(data map[string]interface{}) *Object { + val := &Value{data: data, exists: true} + obj, _ := val.Object() + return obj +} + +func NewObject() *Object { + val := &Value{data: make(map[string]interface{}), exists: true} + obj, _ := val.Object() + return obj +} + +// Creates a new value from an io.reader. +// Returns an error if the reader does not contain valid json. +// Useful for parsing the body of a net/http response. +// Example: NewFromReader(res.Body) +func NewValueFromReader(reader io.Reader) (*Value, error) { + j := new(Value) + d := json.NewDecoder(reader) + d.UseNumber() + err := d.Decode(&j.data) + return j, err +} + +// Creates a new value from bytes. +// Returns an error if the bytes are not valid json. +func NewValueFromBytes(b []byte) (*Value, error) { + r := bytes.NewReader(b) + return NewValueFromReader(r) +} + +func objectFromValue(v *Value, err error) (*Object, error) { + if err != nil { + return nil, err + } + + o, err := v.Object() + + if err != nil { + return nil, err + } + + return o, nil +} + +func NewObjectFromBytes(b []byte) (*Object, error) { + return objectFromValue(NewValueFromBytes(b)) +} + +func NewObjectFromReader(reader io.Reader) (*Object, error) { + return objectFromValue(NewValueFromReader(reader)) +} + +// Marshal into bytes. +func (v *Value) Marshal() ([]byte, error) { + return json.Marshal(v.data) +} + +// Get the interyling data as interface +func (v *Value) Interface() interface{} { + return v.data +} + +func (v *Value) StringMap() map[string]interface{} { + return v.data.(map[string]interface{}) +} + +// Private Get +func (v *Value) get(key string) (*Value, error) { + + // Assume this is an object + obj, err := v.Object() + + if err == nil { + child, ok := obj.Map()[key] + if ok { + return child, nil + } else { + return nil, KeyNotFoundError{key} + } + } + + return nil, err +} + +// Private get path +func (v *Value) getPath(keys []string) (*Value, error) { + current := v + var err error + for _, key := range keys { + current, err = current.get(key) + + if err != nil { + return nil, err + } + } + return current, nil +} + +// Gets the value at key path. +// Returns error if the value does not exist. +// Consider using the more specific Get(..) methods instead. +// Example: +// value, err := GetValue("address", "street") +func (v *Object) GetValue(keys ...string) (*Value, error) { + return v.getPath(keys) +} + +// Gets the value at key path and attempts to typecast the value into an object. +// Returns error if the value is not a json object. +// Example: +// object, err := GetObject("person", "address") +func (v *Object) GetObject(keys ...string) (*Object, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + + obj, err := child.Object() + + if err != nil { + return nil, err + } else { + return obj, nil + } + + } +} + +// Gets the value at key path and attempts to typecast the value into a string. +// Returns error if the value is not a json string. +// Example: +// string, err := GetString("address", "street") +func (v *Object) GetString(keys ...string) (string, error) { + child, err := v.getPath(keys) + + if err != nil { + return "", err + } else { + return child.String() + } +} + +func (v *Object) MustGetString(path string, def string) string { + keys := strings.Split(path, ".") + if str, err := v.GetString(keys...); err != nil { + return def + } else { + return str + } +} + +// Gets the value at key path and attempts to typecast the value into null. +// Returns error if the value is not json null. +// Example: +// err := GetNull("address", "street") +func (v *Object) GetNull(keys ...string) error { + child, err := v.getPath(keys) + + if err != nil { + return err + } + + return child.Null() +} + +// Gets the value at key path and attempts to typecast the value into a number. +// Returns error if the value is not a json number. +// Example: +// n, err := GetNumber("address", "street_number") +func (v *Object) GetNumber(keys ...string) (json.Number, error) { + child, err := v.getPath(keys) + + if err != nil { + return "", err + } else { + + n, err := child.Number() + + if err != nil { + return "", err + } else { + return n, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into a float64. +// Returns error if the value is not a json number. +// Example: +// n, err := GetNumber("address", "street_number") +func (v *Object) GetFloat64(keys ...string) (float64, error) { + child, err := v.getPath(keys) + + if err != nil { + return 0, err + } else { + + n, err := child.Float64() + + if err != nil { + return 0, err + } else { + return n, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into a float64. +// Returns error if the value is not a json number. +// Example: +// n, err := GetNumber("address", "street_number") +func (v *Object) GetInt64(keys ...string) (int64, error) { + child, err := v.getPath(keys) + + if err != nil { + return 0, err + } else { + + n, err := child.Int64() + + if err != nil { + return 0, err + } else { + return n, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into a float64. +// Returns error if the value is not a json number. +// Example: +// v, err := GetInterface("address", "anything") +func (v *Object) GetInterface(keys ...string) (interface{}, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + return child.Interface(), nil + } +} + +// Gets the value at key path and attempts to typecast the value into a bool. +// Returns error if the value is not a json boolean. +// Example: +// married, err := GetBoolean("person", "married") +func (v *Object) GetBoolean(keys ...string) (bool, error) { + child, err := v.getPath(keys) + + if err != nil { + return false, err + } + + return child.Boolean() +} + +// Gets the value at key path and attempts to typecast the value into an array. +// Returns error if the value is not a json array. +// Consider using the more specific GetArray() since it may reduce later type casts. +// Example: +// friends, err := GetValueArray("person", "friends") +// for i, friend := range friends { +// ... // friend will be of type Value here +// } +func (v *Object) GetValueArray(keys ...string) ([]*Value, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + + return child.Array() + + } +} + +// Gets the value at key path and attempts to typecast the value into an array of objects. +// Returns error if the value is not a json array or if any of the contained objects are not objects. +// Example: +// friends, err := GetObjectArray("person", "friends") +// for i, friend := range friends { +// ... // friend will be of type Object here +// } +func (v *Object) GetObjectArray(keys ...string) ([]*Object, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + + array, err := child.Array() + + if err != nil { + return nil, err + } else { + + typedArray := make([]*Object, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem. + Object() + + if err != nil { + return nil, err + } else { + typedArray[index] = typedArrayItem + } + + } + return typedArray, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into an array of string. +// Returns error if the value is not a json array or if any of the contained objects are not strings. +// Gets the value at key path and attempts to typecast the value into an array of objects. +// Returns error if the value is not a json array or if any of the contained objects are not objects. +// Example: +// friendNames, err := GetStringArray("person", "friend_names") +// for i, friendName := range friendNames { +// ... // friendName will be of type string here +// } +func (v *Object) GetStringArray(keys ...string) ([]string, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + + array, err := child.Array() + + if err != nil { + return nil, err + } else { + + typedArray := make([]string, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem.String() + + if err != nil { + return nil, err + } else { + typedArray[index] = typedArrayItem + } + + } + return typedArray, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into an array of numbers. +// Returns error if the value is not a json array or if any of the contained objects are not numbers. +// Example: +// friendAges, err := GetNumberArray("person", "friend_ages") +// for i, friendAge := range friendAges { +// ... // friendAge will be of type float64 here +// } +func (v *Object) GetNumberArray(keys ...string) ([]json.Number, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + + array, err := child.Array() + + if err != nil { + return nil, err + } else { + + typedArray := make([]json.Number, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem.Number() + + if err != nil { + return nil, err + } else { + typedArray[index] = typedArrayItem + } + + } + return typedArray, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into an array of floats. +// Returns error if the value is not a json array or if any of the contained objects are not numbers. +func (v *Object) GetFloat64Array(keys ...string) ([]float64, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + + array, err := child.Array() + + if err != nil { + return nil, err + } else { + + typedArray := make([]float64, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem.Float64() + + if err != nil { + return nil, err + } else { + typedArray[index] = typedArrayItem + } + + } + return typedArray, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into an array of ints. +// Returns error if the value is not a json array or if any of the contained objects are not numbers. +func (v *Object) GetInt64Array(keys ...string) ([]int64, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + + array, err := child.Array() + + if err != nil { + return nil, err + } else { + + typedArray := make([]int64, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem.Int64() + + if err != nil { + return nil, err + } else { + typedArray[index] = typedArrayItem + } + + } + return typedArray, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into an array of bools. +// Returns error if the value is not a json array or if any of the contained objects are not booleans. +func (v *Object) GetBooleanArray(keys ...string) ([]bool, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + + array, err := child.Array() + + if err != nil { + return nil, err + } else { + + typedArray := make([]bool, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem.Boolean() + + if err != nil { + return nil, err + } else { + typedArray[index] = typedArrayItem + } + + } + return typedArray, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into an array of nulls. +// Returns length, or an error if the value is not a json array or if any of the contained objects are not nulls. +func (v *Object) GetNullArray(keys ...string) (int64, error) { + child, err := v.getPath(keys) + + if err != nil { + return 0, err + } else { + + array, err := child.Array() + + if err != nil { + return 0, err + } else { + + var length int64 = 0 + + for _, arrayItem := range array { + err := arrayItem.Null() + + if err != nil { + return 0, err + } else { + length++ + } + + } + return length, nil + } + } +} + +// Returns an error if the value is not actually null +func (v *Value) Null() error { + var valid bool + + // Check the type of this data + switch v.data.(type) { + case nil: + valid = v.exists // Valid only if j also exists, since other values could possibly also be nil + break + } + + if valid { + return nil + } + + return ErrNotNull + +} + +// Attempts to typecast the current value into an array. +// Returns error if the current value is not a json array. +// Example: +// friendsArray, err := friendsValue.Array() +func (v *Value) Array() ([]*Value, error) { + var valid bool + + // Check the type of this data + switch v.data.(type) { + case []interface{}: + valid = true + break + } + + // Unsure if this is a good way to use slices, it's probably not + var slice []*Value + + if valid { + + for _, element := range v.data.([]interface{}) { + child := Value{element, true} + slice = append(slice, &child) + } + + return slice, nil + } + + return slice, ErrNotArray + +} + +// Attempts to typecast the current value into a number. +// Returns error if the current value is not a json number. +// Example: +// ageNumber, err := ageValue.Number() +func (v *Value) Number() (json.Number, error) { + var valid bool + + // Check the type of this data + switch v.data.(type) { + case json.Number: + valid = true + break + } + + if valid { + return v.data.(json.Number), nil + } + + return "", ErrNotNumber +} + +// Attempts to typecast the current value into a float64. +// Returns error if the current value is not a json number. +// Example: +// percentage, err := v.Float64() +func (v *Value) Float64() (float64, error) { + n, err := v.Number() + + if err != nil { + return 0, err + } + + return n.Float64() +} + +// Attempts to typecast the current value into a int64. +// Returns error if the current value is not a json number. +// Example: +// id, err := v.Int64() +func (v *Value) Int64() (int64, error) { + n, err := v.Number() + + if err != nil { + return 0, err + } + + return n.Int64() +} + +// Attempts to typecast the current value into a bool. +// Returns error if the current value is not a json boolean. +// Example: +// marriedBool, err := marriedValue.Boolean() +func (v *Value) Boolean() (bool, error) { + var valid bool + + // Check the type of this data + switch v.data.(type) { + case bool: + valid = true + break + } + + if valid { + return v.data.(bool), nil + } + + return false, ErrNotBool +} + +// Attempts to typecast the current value into an object. +// Returns error if the current value is not a json object. +// Example: +// friendObject, err := friendValue.Object() +func (v *Value) Object() (*Object, error) { + + var valid bool + + // Check the type of this data + switch v.data.(type) { + case map[string]interface{}: + valid = true + break + } + + if valid { + obj := new(Object) + obj.valid = valid + + m := make(map[string]*Value) + + if valid { + for key, element := range v.data.(map[string]interface{}) { + m[key] = &Value{element, true} + + } + } + + obj.data = v.data + obj.m = m + + return obj, nil + } + + return nil, ErrNotObject +} + +// Attempts to typecast the current value into an object arrau. +// Returns error if the current value is not an array of json objects +// Example: +// friendObjects, err := friendValues.ObjectArray() +func (v *Value) ObjectArray() ([]*Object, error) { + + var valid bool + + // Check the type of this data + switch v.data.(type) { + case []interface{}: + valid = true + break + } + + // Unsure if this is a good way to use slices, it's probably not + var slice []*Object + + if valid { + + for _, element := range v.data.([]interface{}) { + childValue := Value{element, true} + childObject, err := childValue.Object() + + if err != nil { + return nil, ErrNotObjectArray + } + slice = append(slice, childObject) + } + + return slice, nil + } + + return nil, ErrNotObjectArray + +} + +// Attempts to typecast the current value into a string. +// Returns error if the current value is not a json string +// Example: +// nameObject, err := nameValue.String() +func (v *Value) String() (string, error) { + var valid bool + + // Check the type of this data + switch v.data.(type) { + case string: + valid = true + break + } + + if valid { + return v.data.(string), nil + } + + return "", ErrNotString +} + +// Returns the value a json formatted string. +// Note: The method named String() is used by golang's log method for logging. +// Example: +func (v *Object) String() string { + + f, err := json.Marshal(v.data) + if err != nil { + return err.Error() + } + + return string(f) + +} + +func (v *Object) SetValue(key string, value interface{}) *Value { + data := v.Interface().(map[string]interface{}) + data[key] = value + + return &Value{ + data: value, + exists: true, + } +} diff --git a/pkg/components/dynmap/dynmap_test.go b/pkg/components/dynmap/dynmap_test.go new file mode 100644 index 00000000000..cc002ea06e0 --- /dev/null +++ b/pkg/components/dynmap/dynmap_test.go @@ -0,0 +1,313 @@ +// uses code from https://github.com/antonholmquist/jason/blob/master/jason.go +// MIT Licence + +package dynmap + +import ( + "log" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +type Assert struct { + T *testing.T +} + +func NewAssert(t *testing.T) *Assert { + return &Assert{ + T: t, + } +} + +func (assert *Assert) True(value bool, message string) { + if value == false { + log.Panicln("Assert: ", message) + } +} + +func TestFirst(t *testing.T) { + + assert := NewAssert(t) + + testJSON := `{ + "name": "anton", + "age": 29, + "nothing": null, + "true": true, + "false": false, + "list": [ + "first", + "second" + ], + "list2": [ + { + "street": "Street 42", + "city": "Stockholm" + }, + { + "street": "Street 42", + "city": "Stockholm" + } + ], + "address": { + "street": "Street 42", + "city": "Stockholm" + }, + "country": { + "name": "Sweden" + } + }` + + j, err := NewObjectFromBytes([]byte(testJSON)) + + a, err := j.GetObject("address") + assert.True(a != nil && err == nil, "failed to create json from string") + + assert.True(err == nil, "failed to create json from string") + + s, err := j.GetString("name") + assert.True(s == "anton" && err == nil, "name should be a string") + + s = j.MustGetString("name", "fallback") + assert.True(s == "anton", "must get string") + + s = j.MustGetString("adsasdas", "fallback") + assert.True(s == "fallback", "must get string return fallback") + + s, err = j.GetString("name") + assert.True(s == "anton" && err == nil, "name shoud match") + + s, err = j.GetString("address", "street") + assert.True(s == "Street 42" && err == nil, "street shoud match") + //log.Println("s: ", s.String()) + + _, err = j.GetNumber("age") + assert.True(err == nil, "age should be a number") + + n, err := j.GetInt64("age") + assert.True(n == 29 && err == nil, "age mismatch") + + ageInterface, err := j.GetInterface("age") + assert.True(ageInterface != nil, "should be defined") + assert.True(err == nil, "age interface error") + + invalidInterface, err := j.GetInterface("not_existing") + assert.True(invalidInterface == nil, "should not give error here") + assert.True(err != nil, "should give error here") + + age, err := j.GetValue("age") + assert.True(age != nil && err == nil, "age should exist") + + age2, err := j.GetValue("age2") + assert.True(age2 == nil && err != nil, "age2 should not exist") + + address, err := j.GetObject("address") + assert.True(address != nil && err == nil, "address should be an object") + + //log.Println("address: ", address) + + s, err = address.GetString("street") + + addressAsString, err := j.GetString("address") + assert.True(addressAsString == "" && err != nil, "address should not be an string") + + s, err = j.GetString("address", "street") + assert.True(s == "Street 42" && err == nil, "street mismatching") + + s, err = j.GetString("address", "name2") + assert.True(s == "" && err != nil, "nonexistent string fail") + + b, err := j.GetBoolean("true") + assert.True(b == true && err == nil, "bool true test") + + b, err = j.GetBoolean("false") + assert.True(b == false && err == nil, "bool false test") + + b, err = j.GetBoolean("invalid_field") + assert.True(b == false && err != nil, "bool invalid test") + + list, err := j.GetValueArray("list") + assert.True(list != nil && err == nil, "list should be an array") + + list2, err := j.GetValueArray("list2") + assert.True(list2 != nil && err == nil, "list2 should be an array") + + list2Array, err := j.GetValueArray("list2") + assert.True(err == nil, "List2 should not return error on AsArray") + assert.True(len(list2Array) == 2, "List2 should should have length 2") + + list2Value, err := j.GetValue("list2") + assert.True(err == nil, "List2 should not return error on value") + + list2ObjectArray, err := list2Value.ObjectArray() + assert.True(err == nil, "list2Value should not return error on ObjectArray") + assert.True(len(list2ObjectArray) == 2, "list2ObjectArray should should have length 2") + + for _, elementValue := range list2Array { + //assert.True(element.IsObject() == true, "first fail") + + element, err := elementValue.Object() + + s, err = element.GetString("street") + assert.True(s == "Street 42" && err == nil, "second fail") + } + + obj, err := j.GetObject("country") + assert.True(obj != nil && err == nil, "country should not return error on AsObject") + for key, value := range obj.Map() { + + assert.True(key == "name", "country name key incorrect") + + s, err = value.String() + assert.True(s == "Sweden" && err == nil, "country name should be Sweden") + } +} + +func TestSecond(t *testing.T) { + json := ` + { + "data": [ + { + "id": "X999_Y999", + "from": { + "name": "Tom Brady", "id": "X12" + }, + "message": "Looking forward to 2010!", + "actions": [ + { + "name": "Comment", + "link": "http://www.facebook.com/X999/posts/Y999" + }, + { + "name": "Like", + "link": "http://www.facebook.com/X999/posts/Y999" + } + ], + "type": "status", + "created_time": "2010-08-02T21:27:44+0000", + "updated_time": "2010-08-02T21:27:44+0000" + }, + { + "id": "X998_Y998", + "from": { + "name": "Peyton Manning", "id": "X18" + }, + "message": "Where's my contract?", + "actions": [ + { + "name": "Comment", + "link": "http://www.facebook.com/X998/posts/Y998" + }, + { + "name": "Like", + "link": "http://www.facebook.com/X998/posts/Y998" + } + ], + "type": "status", + "created_time": "2010-08-02T21:27:44+0000", + "updated_time": "2010-08-02T21:27:44+0000" + } + ] + }` + + assert := NewAssert(t) + j, err := NewObjectFromBytes([]byte(json)) + + assert.True(j != nil && err == nil, "failed to parse json") + + dataObject, err := j.GetObject("data") + assert.True(dataObject == nil && err != nil, "data should not be an object") + + dataArray, err := j.GetObjectArray("data") + assert.True(dataArray != nil && err == nil, "data should be an object array") + + for index, dataItem := range dataArray { + + if index == 0 { + id, err := dataItem.GetString("id") + assert.True(id == "X999_Y999" && err == nil, "item id mismatch") + + fromName, err := dataItem.GetString("from", "name") + assert.True(fromName == "Tom Brady" && err == nil, "fromName mismatch") + + actions, err := dataItem.GetObjectArray("actions") + + for index, action := range actions { + + if index == 1 { + name, err := action.GetString("name") + assert.True(name == "Like" && err == nil, "name mismatch") + + link, err := action.GetString("link") + assert.True(link == "http://www.facebook.com/X999/posts/Y999" && err == nil, "Like mismatch") + + } + + } + } else if index == 1 { + id, err := dataItem.GetString("id") + assert.True(id == "X998_Y998" && err == nil, "item id mismatch") + } + + } + +} + +func TestErrors(t *testing.T) { + json := ` + { + "string": "hello", + "number": 1, + "array": [1,2,3] + }` + + errstr := "expected an error getting %s, but got '%s'" + + j, err := NewObjectFromBytes([]byte(json)) + if err != nil { + t.Fatal("failed to parse json") + } + + if _, err = j.GetObject("string"); err != ErrNotObject { + t.Errorf(errstr, "object", err) + } + + if err = j.GetNull("string"); err != ErrNotNull { + t.Errorf(errstr, "null", err) + } + + if _, err = j.GetStringArray("string"); err != ErrNotArray { + t.Errorf(errstr, "array", err) + } + + if _, err = j.GetStringArray("array"); err != ErrNotString { + t.Errorf(errstr, "string array", err) + } + + if _, err = j.GetNumber("array"); err != ErrNotNumber { + t.Errorf(errstr, "number", err) + } + + if _, err = j.GetBoolean("array"); err != ErrNotBool { + t.Errorf(errstr, "boolean", err) + } + + if _, err = j.GetString("number"); err != ErrNotString { + t.Errorf(errstr, "string", err) + } + + _, err = j.GetString("not_found") + if e, ok := err.(KeyNotFoundError); !ok { + t.Errorf(errstr, "key not found error", e) + } + +} + +func TestWriting(t *testing.T) { + Convey("When writing", t, func() { + j, _ := NewObjectFromBytes([]byte(`{}`)) + j.SetValue("prop", "value") + So(j.MustGetString("prop", ""), ShouldEqual, "value") + }) +} diff --git a/pkg/components/simplejson/simplejson.go b/pkg/components/simplejson/simplejson.go new file mode 100644 index 00000000000..85e2f955943 --- /dev/null +++ b/pkg/components/simplejson/simplejson.go @@ -0,0 +1,468 @@ +package simplejson + +import ( + "bytes" + "encoding/json" + "errors" + "log" +) + +// returns the current implementation version +func Version() string { + return "0.5.0" +} + +type Json struct { + data interface{} +} + +func (j *Json) FromDB(data []byte) error { + j.data = make(map[string]interface{}) + + dec := json.NewDecoder(bytes.NewBuffer(data)) + dec.UseNumber() + return dec.Decode(&j.data) +} + +func (j *Json) ToDB() ([]byte, error) { + if j == nil || j.data == nil { + return nil, nil + } + + return j.Encode() +} + +// NewJson returns a pointer to a new `Json` object +// after unmarshaling `body` bytes +func NewJson(body []byte) (*Json, error) { + j := new(Json) + err := j.UnmarshalJSON(body) + if err != nil { + return nil, err + } + return j, nil +} + +// New returns a pointer to a new, empty `Json` object +func New() *Json { + return &Json{ + data: make(map[string]interface{}), + } +} + +// New returns a pointer to a new, empty `Json` object +func NewFromAny(data interface{}) *Json { + return &Json{data: data} +} + +// Interface returns the underlying data +func (j *Json) Interface() interface{} { + return j.data +} + +// Encode returns its marshaled data as `[]byte` +func (j *Json) Encode() ([]byte, error) { + return j.MarshalJSON() +} + +// EncodePretty returns its marshaled data as `[]byte` with indentation +func (j *Json) EncodePretty() ([]byte, error) { + return json.MarshalIndent(&j.data, "", " ") +} + +// Implements the json.Marshaler interface. +func (j *Json) MarshalJSON() ([]byte, error) { + return json.Marshal(&j.data) +} + +// Set modifies `Json` map by `key` and `value` +// Useful for changing single key/value in a `Json` object easily. +func (j *Json) Set(key string, val interface{}) { + m, err := j.Map() + if err != nil { + return + } + m[key] = val +} + +// SetPath modifies `Json`, recursively checking/creating map keys for the supplied path, +// and then finally writing in the value +func (j *Json) SetPath(branch []string, val interface{}) { + if len(branch) == 0 { + j.data = val + return + } + + // in order to insert our branch, we need map[string]interface{} + if _, ok := (j.data).(map[string]interface{}); !ok { + // have to replace with something suitable + j.data = make(map[string]interface{}) + } + curr := j.data.(map[string]interface{}) + + for i := 0; i < len(branch)-1; i++ { + b := branch[i] + // key exists? + if _, ok := curr[b]; !ok { + n := make(map[string]interface{}) + curr[b] = n + curr = n + continue + } + + // make sure the value is the right sort of thing + if _, ok := curr[b].(map[string]interface{}); !ok { + // have to replace with something suitable + n := make(map[string]interface{}) + curr[b] = n + } + + curr = curr[b].(map[string]interface{}) + } + + // add remaining k/v + curr[branch[len(branch)-1]] = val +} + +// Del modifies `Json` map by deleting `key` if it is present. +func (j *Json) Del(key string) { + m, err := j.Map() + if err != nil { + return + } + delete(m, key) +} + +// Get returns a pointer to a new `Json` object +// for `key` in its `map` representation +// +// useful for chaining operations (to traverse a nested JSON): +// js.Get("top_level").Get("dict").Get("value").Int() +func (j *Json) Get(key string) *Json { + m, err := j.Map() + if err == nil { + if val, ok := m[key]; ok { + return &Json{val} + } + } + return &Json{nil} +} + +// GetPath searches for the item as specified by the branch +// without the need to deep dive using Get()'s. +// +// js.GetPath("top_level", "dict") +func (j *Json) GetPath(branch ...string) *Json { + jin := j + for _, p := range branch { + jin = jin.Get(p) + } + return jin +} + +// GetIndex returns a pointer to a new `Json` object +// for `index` in its `array` representation +// +// this is the analog to Get when accessing elements of +// a json array instead of a json object: +// js.Get("top_level").Get("array").GetIndex(1).Get("key").Int() +func (j *Json) GetIndex(index int) *Json { + a, err := j.Array() + if err == nil { + if len(a) > index { + return &Json{a[index]} + } + } + return &Json{nil} +} + +// CheckGet returns a pointer to a new `Json` object and +// a `bool` identifying success or failure +// +// useful for chained operations when success is important: +// if data, ok := js.Get("top_level").CheckGet("inner"); ok { +// log.Println(data) +// } +func (j *Json) CheckGet(key string) (*Json, bool) { + m, err := j.Map() + if err == nil { + if val, ok := m[key]; ok { + return &Json{val}, true + } + } + return nil, false +} + +// Map type asserts to `map` +func (j *Json) Map() (map[string]interface{}, error) { + if m, ok := (j.data).(map[string]interface{}); ok { + return m, nil + } + return nil, errors.New("type assertion to map[string]interface{} failed") +} + +// Array type asserts to an `array` +func (j *Json) Array() ([]interface{}, error) { + if a, ok := (j.data).([]interface{}); ok { + return a, nil + } + return nil, errors.New("type assertion to []interface{} failed") +} + +// Bool type asserts to `bool` +func (j *Json) Bool() (bool, error) { + if s, ok := (j.data).(bool); ok { + return s, nil + } + return false, errors.New("type assertion to bool failed") +} + +// String type asserts to `string` +func (j *Json) String() (string, error) { + if s, ok := (j.data).(string); ok { + return s, nil + } + return "", errors.New("type assertion to string failed") +} + +// Bytes type asserts to `[]byte` +func (j *Json) Bytes() ([]byte, error) { + if s, ok := (j.data).(string); ok { + return []byte(s), nil + } + return nil, errors.New("type assertion to []byte failed") +} + +// StringArray type asserts to an `array` of `string` +func (j *Json) StringArray() ([]string, error) { + arr, err := j.Array() + if err != nil { + return nil, err + } + retArr := make([]string, 0, len(arr)) + for _, a := range arr { + if a == nil { + retArr = append(retArr, "") + continue + } + s, ok := a.(string) + if !ok { + return nil, err + } + retArr = append(retArr, s) + } + return retArr, nil +} + +// MustArray guarantees the return of a `[]interface{}` (with optional default) +// +// useful when you want to interate over array values in a succinct manner: +// for i, v := range js.Get("results").MustArray() { +// fmt.Println(i, v) +// } +func (j *Json) MustArray(args ...[]interface{}) []interface{} { + var def []interface{} + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustArray() received too many arguments %d", len(args)) + } + + a, err := j.Array() + if err == nil { + return a + } + + return def +} + +// MustMap guarantees the return of a `map[string]interface{}` (with optional default) +// +// useful when you want to interate over map values in a succinct manner: +// for k, v := range js.Get("dictionary").MustMap() { +// fmt.Println(k, v) +// } +func (j *Json) MustMap(args ...map[string]interface{}) map[string]interface{} { + var def map[string]interface{} + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustMap() received too many arguments %d", len(args)) + } + + a, err := j.Map() + if err == nil { + return a + } + + return def +} + +// MustString guarantees the return of a `string` (with optional default) +// +// useful when you explicitly want a `string` in a single value return context: +// myFunc(js.Get("param1").MustString(), js.Get("optional_param").MustString("my_default")) +func (j *Json) MustString(args ...string) string { + var def string + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustString() received too many arguments %d", len(args)) + } + + s, err := j.String() + if err == nil { + return s + } + + return def +} + +// MustStringArray guarantees the return of a `[]string` (with optional default) +// +// useful when you want to interate over array values in a succinct manner: +// for i, s := range js.Get("results").MustStringArray() { +// fmt.Println(i, s) +// } +func (j *Json) MustStringArray(args ...[]string) []string { + var def []string + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustStringArray() received too many arguments %d", len(args)) + } + + a, err := j.StringArray() + if err == nil { + return a + } + + return def +} + +// MustInt guarantees the return of an `int` (with optional default) +// +// useful when you explicitly want an `int` in a single value return context: +// myFunc(js.Get("param1").MustInt(), js.Get("optional_param").MustInt(5150)) +func (j *Json) MustInt(args ...int) int { + var def int + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustInt() received too many arguments %d", len(args)) + } + + i, err := j.Int() + if err == nil { + return i + } + + return def +} + +// MustFloat64 guarantees the return of a `float64` (with optional default) +// +// useful when you explicitly want a `float64` in a single value return context: +// myFunc(js.Get("param1").MustFloat64(), js.Get("optional_param").MustFloat64(5.150)) +func (j *Json) MustFloat64(args ...float64) float64 { + var def float64 + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustFloat64() received too many arguments %d", len(args)) + } + + f, err := j.Float64() + if err == nil { + return f + } + + return def +} + +// MustBool guarantees the return of a `bool` (with optional default) +// +// useful when you explicitly want a `bool` in a single value return context: +// myFunc(js.Get("param1").MustBool(), js.Get("optional_param").MustBool(true)) +func (j *Json) MustBool(args ...bool) bool { + var def bool + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustBool() received too many arguments %d", len(args)) + } + + b, err := j.Bool() + if err == nil { + return b + } + + return def +} + +// MustInt64 guarantees the return of an `int64` (with optional default) +// +// useful when you explicitly want an `int64` in a single value return context: +// myFunc(js.Get("param1").MustInt64(), js.Get("optional_param").MustInt64(5150)) +func (j *Json) MustInt64(args ...int64) int64 { + var def int64 + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustInt64() received too many arguments %d", len(args)) + } + + i, err := j.Int64() + if err == nil { + return i + } + + return def +} + +// MustUInt64 guarantees the return of an `uint64` (with optional default) +// +// useful when you explicitly want an `uint64` in a single value return context: +// myFunc(js.Get("param1").MustUint64(), js.Get("optional_param").MustUint64(5150)) +func (j *Json) MustUint64(args ...uint64) uint64 { + var def uint64 + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustUint64() received too many arguments %d", len(args)) + } + + i, err := j.Uint64() + if err == nil { + return i + } + + return def +} diff --git a/pkg/components/simplejson/simplejson_go11.go b/pkg/components/simplejson/simplejson_go11.go new file mode 100644 index 00000000000..1c479532cf0 --- /dev/null +++ b/pkg/components/simplejson/simplejson_go11.go @@ -0,0 +1,89 @@ +// +build go1.1 + +package simplejson + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "reflect" + "strconv" +) + +// Implements the json.Unmarshaler interface. +func (j *Json) UnmarshalJSON(p []byte) error { + dec := json.NewDecoder(bytes.NewBuffer(p)) + dec.UseNumber() + return dec.Decode(&j.data) +} + +// NewFromReader returns a *Json by decoding from an io.Reader +func NewFromReader(r io.Reader) (*Json, error) { + j := new(Json) + dec := json.NewDecoder(r) + dec.UseNumber() + err := dec.Decode(&j.data) + return j, err +} + +// Float64 coerces into a float64 +func (j *Json) Float64() (float64, error) { + switch j.data.(type) { + case json.Number: + return j.data.(json.Number).Float64() + case float32, float64: + return reflect.ValueOf(j.data).Float(), nil + case int, int8, int16, int32, int64: + return float64(reflect.ValueOf(j.data).Int()), nil + case uint, uint8, uint16, uint32, uint64: + return float64(reflect.ValueOf(j.data).Uint()), nil + } + return 0, errors.New("invalid value type") +} + +// Int coerces into an int +func (j *Json) Int() (int, error) { + switch j.data.(type) { + case json.Number: + i, err := j.data.(json.Number).Int64() + return int(i), err + case float32, float64: + return int(reflect.ValueOf(j.data).Float()), nil + case int, int8, int16, int32, int64: + return int(reflect.ValueOf(j.data).Int()), nil + case uint, uint8, uint16, uint32, uint64: + return int(reflect.ValueOf(j.data).Uint()), nil + } + return 0, errors.New("invalid value type") +} + +// Int64 coerces into an int64 +func (j *Json) Int64() (int64, error) { + switch j.data.(type) { + case json.Number: + return j.data.(json.Number).Int64() + case float32, float64: + return int64(reflect.ValueOf(j.data).Float()), nil + case int, int8, int16, int32, int64: + return reflect.ValueOf(j.data).Int(), nil + case uint, uint8, uint16, uint32, uint64: + return int64(reflect.ValueOf(j.data).Uint()), nil + } + return 0, errors.New("invalid value type") +} + +// Uint64 coerces into an uint64 +func (j *Json) Uint64() (uint64, error) { + switch j.data.(type) { + case json.Number: + return strconv.ParseUint(j.data.(json.Number).String(), 10, 64) + case float32, float64: + return uint64(reflect.ValueOf(j.data).Float()), nil + case int, int8, int16, int32, int64: + return uint64(reflect.ValueOf(j.data).Int()), nil + case uint, uint8, uint16, uint32, uint64: + return reflect.ValueOf(j.data).Uint(), nil + } + return 0, errors.New("invalid value type") +} diff --git a/pkg/components/simplejson/simplejson_test.go b/pkg/components/simplejson/simplejson_test.go new file mode 100644 index 00000000000..b46ffff1873 --- /dev/null +++ b/pkg/components/simplejson/simplejson_test.go @@ -0,0 +1,248 @@ +package simplejson + +import ( + "encoding/json" + "testing" + + "github.com/bmizerany/assert" +) + +func TestSimplejson(t *testing.T) { + var ok bool + var err error + + js, err := NewJson([]byte(`{ + "test": { + "string_array": ["asdf", "ghjk", "zxcv"], + "string_array_null": ["abc", null, "efg"], + "array": [1, "2", 3], + "arraywithsubs": [{"subkeyone": 1}, + {"subkeytwo": 2, "subkeythree": 3}], + "int": 10, + "float": 5.150, + "string": "simplejson", + "bool": true, + "sub_obj": {"a": 1} + } + }`)) + + assert.NotEqual(t, nil, js) + assert.Equal(t, nil, err) + + _, ok = js.CheckGet("test") + assert.Equal(t, true, ok) + + _, ok = js.CheckGet("missing_key") + assert.Equal(t, false, ok) + + aws := js.Get("test").Get("arraywithsubs") + assert.NotEqual(t, nil, aws) + var awsval int + awsval, _ = aws.GetIndex(0).Get("subkeyone").Int() + assert.Equal(t, 1, awsval) + awsval, _ = aws.GetIndex(1).Get("subkeytwo").Int() + assert.Equal(t, 2, awsval) + awsval, _ = aws.GetIndex(1).Get("subkeythree").Int() + assert.Equal(t, 3, awsval) + + i, _ := js.Get("test").Get("int").Int() + assert.Equal(t, 10, i) + + f, _ := js.Get("test").Get("float").Float64() + assert.Equal(t, 5.150, f) + + s, _ := js.Get("test").Get("string").String() + assert.Equal(t, "simplejson", s) + + b, _ := js.Get("test").Get("bool").Bool() + assert.Equal(t, true, b) + + mi := js.Get("test").Get("int").MustInt() + assert.Equal(t, 10, mi) + + mi2 := js.Get("test").Get("missing_int").MustInt(5150) + assert.Equal(t, 5150, mi2) + + ms := js.Get("test").Get("string").MustString() + assert.Equal(t, "simplejson", ms) + + ms2 := js.Get("test").Get("missing_string").MustString("fyea") + assert.Equal(t, "fyea", ms2) + + ma2 := js.Get("test").Get("missing_array").MustArray([]interface{}{"1", 2, "3"}) + assert.Equal(t, ma2, []interface{}{"1", 2, "3"}) + + msa := js.Get("test").Get("string_array").MustStringArray() + assert.Equal(t, msa[0], "asdf") + assert.Equal(t, msa[1], "ghjk") + assert.Equal(t, msa[2], "zxcv") + + msa2 := js.Get("test").Get("string_array").MustStringArray([]string{"1", "2", "3"}) + assert.Equal(t, msa2[0], "asdf") + assert.Equal(t, msa2[1], "ghjk") + assert.Equal(t, msa2[2], "zxcv") + + msa3 := js.Get("test").Get("missing_array").MustStringArray([]string{"1", "2", "3"}) + assert.Equal(t, msa3, []string{"1", "2", "3"}) + + mm2 := js.Get("test").Get("missing_map").MustMap(map[string]interface{}{"found": false}) + assert.Equal(t, mm2, map[string]interface{}{"found": false}) + + strs, err := js.Get("test").Get("string_array").StringArray() + assert.Equal(t, err, nil) + assert.Equal(t, strs[0], "asdf") + assert.Equal(t, strs[1], "ghjk") + assert.Equal(t, strs[2], "zxcv") + + strs2, err := js.Get("test").Get("string_array_null").StringArray() + assert.Equal(t, err, nil) + assert.Equal(t, strs2[0], "abc") + assert.Equal(t, strs2[1], "") + assert.Equal(t, strs2[2], "efg") + + gp, _ := js.GetPath("test", "string").String() + assert.Equal(t, "simplejson", gp) + + gp2, _ := js.GetPath("test", "int").Int() + assert.Equal(t, 10, gp2) + + assert.Equal(t, js.Get("test").Get("bool").MustBool(), true) + + js.Set("float2", 300.0) + assert.Equal(t, js.Get("float2").MustFloat64(), 300.0) + + js.Set("test2", "setTest") + assert.Equal(t, "setTest", js.Get("test2").MustString()) + + js.Del("test2") + assert.NotEqual(t, "setTest", js.Get("test2").MustString()) + + js.Get("test").Get("sub_obj").Set("a", 2) + assert.Equal(t, 2, js.Get("test").Get("sub_obj").Get("a").MustInt()) + + js.GetPath("test", "sub_obj").Set("a", 3) + assert.Equal(t, 3, js.GetPath("test", "sub_obj", "a").MustInt()) +} + +func TestStdlibInterfaces(t *testing.T) { + val := new(struct { + Name string `json:"name"` + Params *Json `json:"params"` + }) + val2 := new(struct { + Name string `json:"name"` + Params *Json `json:"params"` + }) + + raw := `{"name":"myobject","params":{"string":"simplejson"}}` + + assert.Equal(t, nil, json.Unmarshal([]byte(raw), val)) + + assert.Equal(t, "myobject", val.Name) + assert.NotEqual(t, nil, val.Params.data) + s, _ := val.Params.Get("string").String() + assert.Equal(t, "simplejson", s) + + p, err := json.Marshal(val) + assert.Equal(t, nil, err) + assert.Equal(t, nil, json.Unmarshal(p, val2)) + assert.Equal(t, val, val2) // stable +} + +func TestSet(t *testing.T) { + js, err := NewJson([]byte(`{}`)) + assert.Equal(t, nil, err) + + js.Set("baz", "bing") + + s, err := js.GetPath("baz").String() + assert.Equal(t, nil, err) + assert.Equal(t, "bing", s) +} + +func TestReplace(t *testing.T) { + js, err := NewJson([]byte(`{}`)) + assert.Equal(t, nil, err) + + err = js.UnmarshalJSON([]byte(`{"baz":"bing"}`)) + assert.Equal(t, nil, err) + + s, err := js.GetPath("baz").String() + assert.Equal(t, nil, err) + assert.Equal(t, "bing", s) +} + +func TestSetPath(t *testing.T) { + js, err := NewJson([]byte(`{}`)) + assert.Equal(t, nil, err) + + js.SetPath([]string{"foo", "bar"}, "baz") + + s, err := js.GetPath("foo", "bar").String() + assert.Equal(t, nil, err) + assert.Equal(t, "baz", s) +} + +func TestSetPathNoPath(t *testing.T) { + js, err := NewJson([]byte(`{"some":"data","some_number":1.0,"some_bool":false}`)) + assert.Equal(t, nil, err) + + f := js.GetPath("some_number").MustFloat64(99.0) + assert.Equal(t, f, 1.0) + + js.SetPath([]string{}, map[string]interface{}{"foo": "bar"}) + + s, err := js.GetPath("foo").String() + assert.Equal(t, nil, err) + assert.Equal(t, "bar", s) + + f = js.GetPath("some_number").MustFloat64(99.0) + assert.Equal(t, f, 99.0) +} + +func TestPathWillAugmentExisting(t *testing.T) { + js, err := NewJson([]byte(`{"this":{"a":"aa","b":"bb","c":"cc"}}`)) + assert.Equal(t, nil, err) + + js.SetPath([]string{"this", "d"}, "dd") + + cases := []struct { + path []string + outcome string + }{ + { + path: []string{"this", "a"}, + outcome: "aa", + }, + { + path: []string{"this", "b"}, + outcome: "bb", + }, + { + path: []string{"this", "c"}, + outcome: "cc", + }, + { + path: []string{"this", "d"}, + outcome: "dd", + }, + } + + for _, tc := range cases { + s, err := js.GetPath(tc.path...).String() + assert.Equal(t, nil, err) + assert.Equal(t, tc.outcome, s) + } +} + +func TestPathWillOverwriteExisting(t *testing.T) { + // notice how "a" is 0.1 - but then we'll try to set at path a, foo + js, err := NewJson([]byte(`{"this":{"a":0.1,"b":"bb","c":"cc"}}`)) + assert.Equal(t, nil, err) + + js.SetPath([]string{"this", "a", "foo"}, "bar") + + s, err := js.GetPath("this", "a", "foo").String() + assert.Equal(t, nil, err) + assert.Equal(t, "bar", s) +} diff --git a/pkg/models/dashboard_snapshot.go b/pkg/models/dashboard_snapshot.go index 9bfbd06c1ef..f920e91f2e4 100644 --- a/pkg/models/dashboard_snapshot.go +++ b/pkg/models/dashboard_snapshot.go @@ -1,6 +1,10 @@ package models -import "time" +import ( + "time" + + "github.com/grafana/grafana/pkg/components/simplejson" +) // DashboardSnapshot model type DashboardSnapshot struct { @@ -17,7 +21,7 @@ type DashboardSnapshot struct { Created time.Time Updated time.Time - Dashboard map[string]interface{} + Dashboard *simplejson.Json } // DashboardSnapshotDTO without dashboard map @@ -40,9 +44,9 @@ type DashboardSnapshotDTO struct { // COMMANDS type CreateDashboardSnapshotCommand struct { - Dashboard map[string]interface{} `json:"dashboard" binding:"Required"` - Name string `json:"name" binding:"Required"` - Expires int64 `json:"expires"` + Dashboard *simplejson.Json `json:"dashboard" binding:"Required"` + Name string `json:"name" binding:"Required"` + Expires int64 `json:"expires"` // these are passed when storing an external snapshot ref External bool `json:"external"` diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 3e87f504a77..6243c729624 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -6,6 +6,7 @@ import ( "time" "github.com/gosimple/slug" + "github.com/grafana/grafana/pkg/components/simplejson" ) // Typed errors @@ -37,14 +38,14 @@ type Dashboard struct { CreatedBy int64 Title string - Data map[string]interface{} + Data *simplejson.Json } // NewDashboard creates a new dashboard func NewDashboard(title string) *Dashboard { dash := &Dashboard{} - dash.Data = make(map[string]interface{}) - dash.Data["title"] = title + dash.Data = simplejson.New() + dash.Data.Set("title", title) dash.Title = title dash.Created = time.Now() dash.Updated = time.Now() @@ -54,34 +55,24 @@ func NewDashboard(title string) *Dashboard { // GetTags turns the tags in data json into go string array func (dash *Dashboard) GetTags() []string { - jsonTags := dash.Data["tags"] - if jsonTags == nil || jsonTags == "" { - return []string{} - } - - arr := jsonTags.([]interface{}) - b := make([]string, len(arr)) - for i := range arr { - b[i] = arr[i].(string) - } - return b + return dash.Data.Get("tags").MustStringArray() } -func NewDashboardFromJson(data map[string]interface{}) *Dashboard { +func NewDashboardFromJson(data *simplejson.Json) *Dashboard { dash := &Dashboard{} dash.Data = data - dash.Title = dash.Data["title"].(string) + dash.Title = dash.Data.Get("title").MustString() dash.UpdateSlug() - if dash.Data["id"] != nil { - dash.Id = int64(dash.Data["id"].(float64)) + if id, err := dash.Data.Get("id").Float64(); err == nil { + dash.Id = int64(id) - if dash.Data["version"] != nil { - dash.Version = int(dash.Data["version"].(float64)) + if version, err := dash.Data.Get("version").Float64(); err == nil { + dash.Version = int(version) dash.Updated = time.Now() } } else { - dash.Data["version"] = 0 + dash.Data.Set("version", 0) dash.Created = time.Now() dash.Updated = time.Now() } @@ -92,9 +83,11 @@ func NewDashboardFromJson(data map[string]interface{}) *Dashboard { // GetDashboardModel turns the command into the savable model func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { dash := NewDashboardFromJson(cmd.Dashboard) - if dash.Data["version"] == 0 { + + if dash.Data.Get("version").MustInt(0) == 0 { dash.CreatedBy = cmd.UserId } + dash.UpdatedBy = cmd.UserId dash.OrgId = cmd.OrgId dash.UpdateSlug() @@ -103,15 +96,12 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { // GetString a func (dash *Dashboard) GetString(prop string, defaultValue string) string { - if val, exists := dash.Data[prop]; exists { - return val.(string) - } - return defaultValue + return dash.Data.Get(prop).MustString(defaultValue) } // UpdateSlug updates the slug func (dash *Dashboard) UpdateSlug() { - title := strings.ToLower(dash.Data["title"].(string)) + title := strings.ToLower(dash.Data.Get("title").MustString()) dash.Slug = slug.Make(title) } @@ -120,10 +110,10 @@ func (dash *Dashboard) UpdateSlug() { // type SaveDashboardCommand struct { - Dashboard map[string]interface{} `json:"dashboard" binding:"Required"` - UserId int64 `json:"userId"` - OrgId int64 `json:"-"` - Overwrite bool `json:"overwrite"` + Dashboard *simplejson.Json `json:"dashboard" binding:"Required"` + UserId int64 `json:"userId"` + OrgId int64 `json:"-"` + Overwrite bool `json:"overwrite"` Result *Dashboard } diff --git a/pkg/models/dashboards_test.go b/pkg/models/dashboards_test.go index b0b6796c4d8..ee16508dc8a 100644 --- a/pkg/models/dashboards_test.go +++ b/pkg/models/dashboards_test.go @@ -3,6 +3,7 @@ package models import ( "testing" + "github.com/grafana/grafana/pkg/components/simplejson" . "github.com/smartystreets/goconvey/convey" ) @@ -16,12 +17,11 @@ func TestDashboardModel(t *testing.T) { }) Convey("Given a dashboard json", t, func() { - json := map[string]interface{}{ - "title": "test dash", - } + json := simplejson.New() + json.Set("title", "test dash") Convey("With tags as string value", func() { - json["tags"] = "" + json.Set("tags", "") dash := NewDashboardFromJson(json) So(len(dash.GetTags()), ShouldEqual, 0) diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index 88abd03c319..2e9d98e9700 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -3,6 +3,8 @@ package models import ( "errors" "time" + + "github.com/grafana/grafana/pkg/components/simplejson" ) const ( @@ -42,7 +44,7 @@ type DataSource struct { BasicAuthPassword string WithCredentials bool IsDefault bool - JsonData map[string]interface{} + JsonData *simplejson.Json Created time.Time Updated time.Time @@ -74,19 +76,19 @@ func IsKnownDataSourcePlugin(dsType string) bool { // Also acts as api DTO type AddDataSourceCommand struct { - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - Access DsAccess `json:"access" binding:"Required"` - Url string `json:"url"` - Password string `json:"password"` - Database string `json:"database"` - User string `json:"user"` - BasicAuth bool `json:"basicAuth"` - BasicAuthUser string `json:"basicAuthUser"` - BasicAuthPassword string `json:"basicAuthPassword"` - WithCredentials bool `json:"withCredentials"` - IsDefault bool `json:"isDefault"` - JsonData map[string]interface{} `json:"jsonData"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + Access DsAccess `json:"access" binding:"Required"` + Url string `json:"url"` + Password string `json:"password"` + Database string `json:"database"` + User string `json:"user"` + BasicAuth bool `json:"basicAuth"` + BasicAuthUser string `json:"basicAuthUser"` + BasicAuthPassword string `json:"basicAuthPassword"` + WithCredentials bool `json:"withCredentials"` + IsDefault bool `json:"isDefault"` + JsonData *simplejson.Json `json:"jsonData"` OrgId int64 `json:"-"` @@ -95,19 +97,19 @@ type AddDataSourceCommand struct { // Also acts as api DTO type UpdateDataSourceCommand struct { - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - Access DsAccess `json:"access" binding:"Required"` - Url string `json:"url"` - Password string `json:"password"` - User string `json:"user"` - Database string `json:"database"` - BasicAuth bool `json:"basicAuth"` - BasicAuthUser string `json:"basicAuthUser"` - BasicAuthPassword string `json:"basicAuthPassword"` - WithCredentials bool `json:"withCredentials"` - IsDefault bool `json:"isDefault"` - JsonData map[string]interface{} `json:"jsonData"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + Access DsAccess `json:"access" binding:"Required"` + Url string `json:"url"` + Password string `json:"password"` + User string `json:"user"` + Database string `json:"database"` + BasicAuth bool `json:"basicAuth"` + BasicAuthUser string `json:"basicAuthUser"` + BasicAuthPassword string `json:"basicAuthPassword"` + WithCredentials bool `json:"withCredentials"` + IsDefault bool `json:"isDefault"` + JsonData *simplejson.Json `json:"jsonData"` OrgId int64 `json:"-"` Id int64 `json:"-"` diff --git a/pkg/plugins/dashboard_importer.go b/pkg/plugins/dashboard_importer.go new file mode 100644 index 00000000000..834bfabd048 --- /dev/null +++ b/pkg/plugins/dashboard_importer.go @@ -0,0 +1,172 @@ +package plugins + +import ( + "encoding/json" + "fmt" + "regexp" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" +) + +type ImportDashboardCommand struct { + Path string `json:"string"` + Inputs []ImportDashboardInput `json:"inputs"` + + OrgId int64 `json:"-"` + UserId int64 `json:"-"` + PluginId string `json:"-"` + Result *PluginDashboardInfoDTO +} + +type ImportDashboardInput struct { + Type string `json:"type"` + PluginId string `json:"pluginId"` + Name string `json:"name"` + Value string `json:"value"` +} + +type DashboardInputMissingError struct { + VariableName string +} + +func (e DashboardInputMissingError) Error() string { + return fmt.Sprintf("Dashbord input variable: %v missing from import command", e.VariableName) +} + +func init() { + bus.AddHandler("plugins", ImportDashboard) +} + +func ImportDashboard(cmd *ImportDashboardCommand) error { + plugin, exists := Plugins[cmd.PluginId] + + if !exists { + return PluginNotFoundError{cmd.PluginId} + } + + var dashboard *m.Dashboard + var err error + + if dashboard, err = loadPluginDashboard(plugin, cmd.Path); err != nil { + return err + } + + evaluator := &DashTemplateEvaluator{ + template: dashboard.Data, + inputs: cmd.Inputs, + } + + generatedDash, err := evaluator.Eval() + if err != nil { + return err + } + + saveCmd := m.SaveDashboardCommand{ + Dashboard: generatedDash, + OrgId: cmd.OrgId, + UserId: cmd.UserId, + } + + if err := bus.Dispatch(&saveCmd); err != nil { + return err + } + + cmd.Result = &PluginDashboardInfoDTO{ + PluginId: cmd.PluginId, + Title: dashboard.Title, + Path: cmd.Path, + Revision: dashboard.GetString("revision", "1.0"), + InstalledUri: "db/" + saveCmd.Result.Slug, + InstalledRevision: dashboard.GetString("revision", "1.0"), + Installed: true, + } + + return nil +} + +type DashTemplateEvaluator struct { + template *simplejson.Json + inputs []ImportDashboardInput + variables map[string]string + result *simplejson.Json + varRegex *regexp.Regexp +} + +func (this *DashTemplateEvaluator) findInput(varName string, varType string) *ImportDashboardInput { + + for _, input := range this.inputs { + if varType == input.Type && (input.Name == varName || input.Name == "*") { + return &input + } + } + + return nil +} + +func (this *DashTemplateEvaluator) Eval() (*simplejson.Json, error) { + this.result = simplejson.New() + this.variables = make(map[string]string) + this.varRegex, _ = regexp.Compile(`(\$\{\w+\})`) + + // check that we have all inputs we need + for _, inputDef := range this.template.Get("__inputs").MustArray() { + inputDefJson := simplejson.NewFromAny(inputDef) + inputName := inputDefJson.Get("name").MustString() + inputType := inputDefJson.Get("type").MustString() + input := this.findInput(inputName, inputType) + + if input == nil { + return nil, &DashboardInputMissingError{VariableName: inputName} + } + + this.variables["${"+inputName+"}"] = input.Value + } + + return simplejson.NewFromAny(this.evalObject(this.template)), nil +} + +func (this *DashTemplateEvaluator) evalValue(source *simplejson.Json) interface{} { + + sourceValue := source.Interface() + + switch v := sourceValue.(type) { + case string: + interpolated := this.varRegex.ReplaceAllStringFunc(v, func(match string) string { + if replacement, exists := this.variables[match]; exists { + return replacement + } else { + return match + } + }) + return interpolated + case bool: + return v + case json.Number: + return v + case map[string]interface{}: + return this.evalObject(source) + case []interface{}: + array := make([]interface{}, 0) + for _, item := range v { + array = append(array, this.evalValue(simplejson.NewFromAny(item))) + } + return array + } + + return nil +} + +func (this *DashTemplateEvaluator) evalObject(source *simplejson.Json) interface{} { + result := make(map[string]interface{}) + + for key, value := range source.MustMap() { + if key == "__inputs" { + continue + } + result[key] = this.evalValue(simplejson.NewFromAny(value)) + } + + return result +} diff --git a/pkg/plugins/dashboard_importer_test.go b/pkg/plugins/dashboard_importer_test.go new file mode 100644 index 00000000000..d2897fad1cd --- /dev/null +++ b/pkg/plugins/dashboard_importer_test.go @@ -0,0 +1,94 @@ +package plugins + +import ( + "io/ioutil" + "testing" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/ini.v1" +) + +func TestDashboardImport(t *testing.T) { + + Convey("When importing plugin dashboard", t, func() { + setting.Cfg = ini.Empty() + sec, _ := setting.Cfg.NewSection("plugin.test-app") + sec.NewKey("path", "../../tests/test-app") + err := Init() + + So(err, ShouldBeNil) + + var importedDash *m.Dashboard + bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error { + importedDash = cmd.GetDashboardModel() + cmd.Result = importedDash + return nil + }) + + cmd := ImportDashboardCommand{ + PluginId: "test-app", + Path: "dashboards/connections.json", + OrgId: 1, + UserId: 1, + Inputs: []ImportDashboardInput{ + {Name: "*", Type: "datasource", Value: "graphite"}, + }, + } + + err = ImportDashboard(&cmd) + So(err, ShouldBeNil) + + Convey("should install dashboard", func() { + So(importedDash, ShouldNotBeNil) + + resultStr, _ := importedDash.Data.EncodePretty() + expectedBytes, _ := ioutil.ReadFile("../../tests/test-app/dashboards/connections_result.json") + expectedJson, _ := simplejson.NewJson(expectedBytes) + expectedStr, _ := expectedJson.EncodePretty() + + So(string(resultStr), ShouldEqual, string(expectedStr)) + + panel := importedDash.Data.Get("rows").GetIndex(0).Get("panels").GetIndex(0) + So(panel.Get("datasource").MustString(), ShouldEqual, "graphite") + }) + }) + + Convey("When evaling dashboard template", t, func() { + template, _ := simplejson.NewJson([]byte(`{ + "__inputs": [ + { + "name": "DS_NAME", + "type": "datasource" + } + ], + "test": { + "prop": "${DS_NAME}" + } + }`)) + + evaluator := &DashTemplateEvaluator{ + template: template, + inputs: []ImportDashboardInput{ + {Name: "*", Type: "datasource", Value: "my-server"}, + }, + } + + res, err := evaluator.Eval() + So(err, ShouldBeNil) + + Convey("should render template", func() { + So(res.GetPath("test", "prop").MustString(), ShouldEqual, "my-server") + }) + + Convey("should not include inputs in output", func() { + inputs := res.Get("__inputs") + So(inputs.Interface(), ShouldBeNil) + }) + + }) + +} diff --git a/pkg/plugins/dashboard_installer.go b/pkg/plugins/dashboard_installer.go deleted file mode 100644 index 279ab209989..00000000000 --- a/pkg/plugins/dashboard_installer.go +++ /dev/null @@ -1,57 +0,0 @@ -package plugins - -import ( - "github.com/grafana/grafana/pkg/bus" - m "github.com/grafana/grafana/pkg/models" -) - -type InstallPluginDashboardCommand struct { - Path string `json:"string"` - Inputs map[string]interface{} `json:"inputs"` - - OrgId int64 `json:"-"` - UserId int64 `json:"-"` - PluginId string `json:"-"` - Result *PluginDashboardInfoDTO -} - -func init() { - bus.AddHandler("plugins", InstallPluginDashboard) -} - -func InstallPluginDashboard(cmd *InstallPluginDashboardCommand) error { - plugin, exists := Plugins[cmd.PluginId] - - if !exists { - return PluginNotFoundError{cmd.PluginId} - } - - var dashboard *m.Dashboard - var err error - - if dashboard, err = loadPluginDashboard(plugin, cmd.Path); err != nil { - return err - } - - saveCmd := m.SaveDashboardCommand{ - Dashboard: dashboard.Data, - OrgId: cmd.OrgId, - UserId: cmd.UserId, - } - - if err := bus.Dispatch(&saveCmd); err != nil { - return err - } - - cmd.Result = &PluginDashboardInfoDTO{ - PluginId: cmd.PluginId, - Title: dashboard.Title, - Path: cmd.Path, - Revision: dashboard.GetString("revision", "1.0"), - InstalledUri: "db/" + saveCmd.Result.Slug, - InstalledRevision: dashboard.GetString("revision", "1.0"), - Installed: true, - } - - return nil -} diff --git a/pkg/plugins/dashboards.go b/pkg/plugins/dashboards.go index 1697ad808f2..932196a42a9 100644 --- a/pkg/plugins/dashboards.go +++ b/pkg/plugins/dashboards.go @@ -1,11 +1,11 @@ package plugins import ( - "encoding/json" "os" "path/filepath" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" ) @@ -52,10 +52,8 @@ func loadPluginDashboard(plugin *PluginBase, path string) (*m.Dashboard, error) defer reader.Close() - jsonParser := json.NewDecoder(reader) - var data map[string]interface{} - - if err := jsonParser.Decode(&data); err != nil { + data, err := simplejson.NewFromReader(reader) + if err != nil { return nil, err } diff --git a/pkg/plugins/dashboards_test.go b/pkg/plugins/dashboards_test.go index 58cbe2f4920..bdd08ceefd2 100644 --- a/pkg/plugins/dashboards_test.go +++ b/pkg/plugins/dashboards_test.go @@ -23,7 +23,7 @@ func TestPluginDashboards(t *testing.T) { bus.AddHandler("test", func(query *m.GetDashboardQuery) error { if query.Slug == "nginx-connections" { dash := m.NewDashboard("Nginx Connections") - dash.Data["revision"] = "1.1" + dash.Data.Set("revision", "1.1") query.Result = dash return nil } diff --git a/pkg/plugins/frontend_plugin.go b/pkg/plugins/frontend_plugin.go index aab3f851f60..9bca4d7f357 100644 --- a/pkg/plugins/frontend_plugin.go +++ b/pkg/plugins/frontend_plugin.go @@ -3,9 +3,9 @@ package plugins import ( "net/url" "path" - "path/filepath" "strings" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -14,26 +14,33 @@ type FrontendPluginBase struct { } func (fp *FrontendPluginBase) initFrontendPlugin() { - if fp.StaticRoot != "" { - fp.StaticRootAbs = filepath.Join(fp.PluginDir, fp.StaticRoot) + if isInternalPlugin(fp.PluginDir) { StaticRoutes = append(StaticRoutes, &PluginStaticRoute{ - Directory: fp.StaticRootAbs, + Directory: fp.PluginDir, PluginId: fp.Id, }) } fp.handleModuleDefaults() - fp.Info.Logos.Small = evalRelativePluginUrlPath(fp.Info.Logos.Small, fp.BaseUrl) - fp.Info.Logos.Large = evalRelativePluginUrlPath(fp.Info.Logos.Large, fp.BaseUrl) + fp.Info.Logos.Small = getPluginLogoUrl(fp.Info.Logos.Small, fp.BaseUrl) + fp.Info.Logos.Large = getPluginLogoUrl(fp.Info.Logos.Large, fp.BaseUrl) for i := 0; i < len(fp.Info.Screenshots); i++ { fp.Info.Screenshots[i].Path = evalRelativePluginUrlPath(fp.Info.Screenshots[i].Path, fp.BaseUrl) } } +func getPluginLogoUrl(path, baseUrl string) string { + if path == "" { + return "public/img/plugin-default-logo_dark.svg" + } + + return evalRelativePluginUrlPath(path, baseUrl) +} + func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) { - appSubPath := strings.Replace(fp.PluginDir, app.StaticRootAbs, "", 1) + appSubPath := strings.Replace(fp.PluginDir, app.PluginDir, "", 1) fp.IncludedInAppId = app.Id fp.BaseUrl = app.BaseUrl fp.Module = util.JoinUrlFragments("plugins/"+app.Id, appSubPath) + "/module" @@ -41,7 +48,7 @@ func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) { func (fp *FrontendPluginBase) handleModuleDefaults() { - if fp.StaticRoot != "" { + if isInternalPlugin(fp.PluginDir) { fp.Module = path.Join("plugins", fp.Id, "module") fp.BaseUrl = path.Join("public/plugins", fp.Id) return @@ -51,6 +58,10 @@ func (fp *FrontendPluginBase) handleModuleDefaults() { fp.BaseUrl = path.Join("public/app/plugins", fp.Type, fp.Id) } +func isInternalPlugin(pluginDir string) bool { + return !strings.Contains(pluginDir, setting.StaticRootPath) +} + func evalRelativePluginUrlPath(pathStr string, baseUrl string) string { if pathStr == "" { return "" diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 502c355eda8..8443e91931d 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -30,19 +30,20 @@ type PluginLoader interface { } type PluginBase struct { - Type string `json:"type"` - Name string `json:"name"` - Id string `json:"id"` - Info PluginInfo `json:"info"` - Dependencies PluginDependencies `json:"dependencies"` - Includes []*PluginInclude `json:"includes"` - Module string `json:"module"` - BaseUrl string `json:"baseUrl"` - StaticRoot string `json:"staticRoot"` - StaticRootAbs string `json:"-"` + Type string `json:"type"` + Name string `json:"name"` + Id string `json:"id"` + Info PluginInfo `json:"info"` + Dependencies PluginDependencies `json:"dependencies"` + Includes []*PluginInclude `json:"includes"` + Module string `json:"module"` + BaseUrl string `json:"baseUrl"` IncludedInAppId string `json:"-"` PluginDir string `json:"-"` + + // cache for readme file contents + Readme []byte `json:"-"` } func (pb *PluginBase) registerPlugin(pluginDir string) error { diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index db674e5522a..0d69d5745b9 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -3,6 +3,7 @@ package plugins import ( "encoding/json" "errors" + "io/ioutil" "os" "path" "path/filepath" @@ -155,3 +156,31 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error { reader.Seek(0, 0) return loader.Load(jsonParser, currentDir) } + +func GetPluginReadme(pluginId string) ([]byte, error) { + plug, exists := Plugins[pluginId] + if !exists { + return nil, PluginNotFoundError{pluginId} + } + + if plug.Readme != nil { + return plug.Readme, nil + } + + readmePath := filepath.Join(plug.PluginDir, "README.md") + if _, err := os.Stat(readmePath); os.IsNotExist(err) { + readmePath = filepath.Join(plug.PluginDir, "readme.md") + } + + if _, err := os.Stat(readmePath); os.IsNotExist(err) { + plug.Readme = make([]byte, 0) + return plug.Readme, nil + } + + if readmeBytes, err := ioutil.ReadFile(readmePath); err != nil { + return nil, err + } else { + plug.Readme = readmeBytes + return plug.Readme, nil + } +} diff --git a/pkg/services/search/json_index.go b/pkg/services/search/json_index.go index e70c662438d..79c238b27f9 100644 --- a/pkg/services/search/json_index.go +++ b/pkg/services/search/json_index.go @@ -1,12 +1,12 @@ package search import ( - "encoding/json" "os" "path/filepath" "strings" "time" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" ) @@ -120,10 +120,8 @@ func loadDashboardFromFile(filename string) (*JsonDashIndexItem, error) { } defer reader.Close() - jsonParser := json.NewDecoder(reader) - var data map[string]interface{} - - if err := jsonParser.Decode(&data); err != nil { + data, err := simplejson.NewFromReader(reader) + if err != nil { return nil, err } diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index a33ef10d725..396d507cfd2 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -69,7 +69,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { affectedRows, err = sess.Insert(dash) } else { dash.Version += 1 - dash.Data["version"] = dash.Version + dash.Data.Set("version", dash.Version) affectedRows, err = sess.Id(dash.Id).Update(dash) } @@ -108,7 +108,7 @@ func GetDashboard(query *m.GetDashboardQuery) error { return m.ErrDashboardNotFound } - dashboard.Data["id"] = dashboard.Id + dashboard.Data.Set("id", dashboard.Id) query.Result = &dashboard return nil diff --git a/pkg/services/sqlstore/dashboard_snapshot_test.go b/pkg/services/sqlstore/dashboard_snapshot_test.go index 5301f0f1cc9..50375088b4b 100644 --- a/pkg/services/sqlstore/dashboard_snapshot_test.go +++ b/pkg/services/sqlstore/dashboard_snapshot_test.go @@ -5,6 +5,7 @@ import ( . "github.com/smartystreets/goconvey/convey" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" ) @@ -16,9 +17,9 @@ func TestDashboardSnapshotDBAccess(t *testing.T) { Convey("Given saved snaphot", func() { cmd := m.CreateDashboardSnapshotCommand{ Key: "hej", - Dashboard: map[string]interface{}{ + Dashboard: simplejson.NewFromAny(map[string]interface{}{ "hello": "mupp", - }, + }), } err := CreateDashboardSnapshot(&cmd) So(err, ShouldBeNil) @@ -29,7 +30,7 @@ func TestDashboardSnapshotDBAccess(t *testing.T) { So(err, ShouldBeNil) So(query.Result, ShouldNotBeNil) - So(query.Result.Dashboard["hello"], ShouldEqual, "mupp") + So(query.Result.Dashboard.Get("hello").MustString(), ShouldEqual, "mupp") }) }) diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 2d35a0742de..609639f7788 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -5,6 +5,7 @@ import ( . "github.com/smartystreets/goconvey/convey" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/search" ) @@ -12,11 +13,11 @@ import ( func insertTestDashboard(title string, orgId int64, tags ...interface{}) *m.Dashboard { cmd := m.SaveDashboardCommand{ OrgId: orgId, - Dashboard: map[string]interface{}{ + Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": title, "tags": tags, - }, + }), } err := SaveDashboard(&cmd) @@ -58,11 +59,11 @@ func TestDashboardDataAccess(t *testing.T) { cmd := m.SaveDashboardCommand{ OrgId: 1, Overwrite: true, - Dashboard: map[string]interface{}{ + Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": float64(123412321), "title": "Expect error", "tags": []interface{}{}, - }, + }), } err := SaveDashboard(&cmd) @@ -76,11 +77,11 @@ func TestDashboardDataAccess(t *testing.T) { cmd := m.SaveDashboardCommand{ OrgId: 2, Overwrite: true, - Dashboard: map[string]interface{}{ + Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": float64(query.Result.Id), "title": "Expect error", "tags": []interface{}{}, - }, + }), } err := SaveDashboard(&cmd) @@ -135,11 +136,11 @@ func TestDashboardDataAccess(t *testing.T) { Convey("Should not be able to save dashboard with same name", func() { cmd := m.SaveDashboardCommand{ OrgId: 1, - Dashboard: map[string]interface{}{ + Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": "test dash 23", "tags": []interface{}{}, - }, + }), } err := SaveDashboard(&cmd) diff --git a/public/app/core/components/layout_selector/layout_selector.ts b/public/app/core/components/layout_selector/layout_selector.ts new file mode 100644 index 00000000000..e9d90aa1f48 --- /dev/null +++ b/public/app/core/components/layout_selector/layout_selector.ts @@ -0,0 +1,74 @@ +/// + +import config from 'app/core/config'; +import store from 'app/core/store'; +import _ from 'lodash'; +import $ from 'jquery'; +import coreModule from 'app/core/core_module'; + +var template = ` +
+ + +
+`; + +export class LayoutSelectorCtrl { + mode: string; + + /** @ngInject **/ + constructor(private $rootScope) { + this.mode = store.get('grafana.list.layout.mode') || 'grid'; + } + + listView() { + this.mode = 'list'; + store.set('grafana.list.layout.mode', 'list'); + this.$rootScope.appEvent('layout-mode-changed', 'list'); + } + + gridView() { + this.mode = 'grid'; + store.set('grafana.list.layout.mode', 'grid'); + this.$rootScope.appEvent('layout-mode-changed', 'grid'); + } + +} + +/** @ngInject **/ +export function layoutSelector() { + return { + restrict: 'E', + controller: LayoutSelectorCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: {}, + template: template, + }; +} + +/** @ngInject **/ +export function layoutMode($rootScope) { + return { + restrict: 'A', + scope: {}, + link: function(scope, elem) { + var layout = store.get('grafana.list.layout.mode') || 'grid'; + var className = 'card-list-layout-' + layout; + elem.addClass(className); + + $rootScope.onAppEvent('layout-mode-changed', (evt, newLayout) => { + elem.removeClass(className); + className = 'card-list-layout-' + newLayout; + elem.addClass(className); + }, scope); + } + }; +} + +coreModule.directive('layoutSelector', layoutSelector); +coreModule.directive('layoutMode', layoutMode); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 32ddee02dc7..2ab7ea01410 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -28,6 +28,7 @@ import {infoPopover} from './components/info_popover'; import {colorPicker} from './components/colorpicker'; import {navbarDirective} from './components/navbar/navbar'; import {arrayJoin} from './directives/array_join'; +import {layoutSelector} from './components/layout_selector/layout_selector'; import 'app/core/controllers/all'; import 'app/core/services/all'; import 'app/core/routes/routes'; @@ -42,5 +43,6 @@ export { navbarDirective, searchDirective, colorPicker, + layoutSelector, infoPopover }; diff --git a/public/app/core/utils/kbn.js b/public/app/core/utils/kbn.js index f6e4277523e..11a544d2708 100644 --- a/public/app/core/utils/kbn.js +++ b/public/app/core/utils/kbn.js @@ -171,7 +171,7 @@ function($, _) { kbn.describe_interval = function (string) { var matches = string.match(kbn.interval_regex); if (!matches || !_.has(kbn.intervals_in_seconds, matches[2])) { - throw new Error('Invalid interval string, expexcting a number followed by one of "Mwdhmsy"'); + throw new Error('Invalid interval string, expecting a number followed by one of "Mwdhmsy"'); } else { return { sec: kbn.intervals_in_seconds[matches[2]], diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index dcc2f40d0dc..2d5b66cb13c 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -137,8 +137,8 @@ export class DashNavCtrl { $scope.deleteDashboard = function() { $scope.appEvent('confirm-modal', { - title: 'Delete dashboard', - text: 'Do you want to delete dashboard?', + title: 'Delete', + text: 'Do you want to delete this dashboard?', text2: $scope.dashboard.title, icon: 'fa-trash', yesText: 'Delete', diff --git a/public/app/features/dashboard/impression_store.ts b/public/app/features/dashboard/impression_store.ts index 4bd979a6b34..a0f20f7ec75 100644 --- a/public/app/features/dashboard/impression_store.ts +++ b/public/app/features/dashboard/impression_store.ts @@ -2,14 +2,16 @@ import store from 'app/core/store'; import _ from 'lodash'; +import config from 'app/core/config'; export class ImpressionsStore { constructor() {} addDashboardImpression(dashboardId) { + var impressionsKey = this.impressionKey(config); var impressions = []; - if (store.exists("dashboard_impressions")) { - impressions = JSON.parse(store.get("dashboard_impressions")); + if (store.exists(impressionsKey)) { + impressions = JSON.parse(store.get(impressionsKey)); if (!_.isArray(impressions)) { impressions = []; } @@ -24,11 +26,11 @@ export class ImpressionsStore { if (impressions.length > 50) { impressions.pop(); } - store.set("dashboard_impressions", JSON.stringify(impressions)); + store.set(impressionsKey, JSON.stringify(impressions)); } getDashboardOpened() { - var impressions = store.get("dashboard_impressions") || "[]"; + var impressions = store.get(this.impressionKey(config)) || "[]"; impressions = JSON.parse(impressions); @@ -38,6 +40,10 @@ export class ImpressionsStore { return impressions; } + + impressionKey(config) { + return "dashboard_impressions-" + config.bootData.user.orgId; + } } var impressions = new ImpressionsStore(); diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts index 1e07b3a814a..d2ca7a82dc4 100644 --- a/public/app/features/plugins/ds_edit_ctrl.ts +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -24,6 +24,7 @@ export class DataSourceEditCtrl { datasourceMeta: any; tabIndex: number; hasDashboards: boolean; + editForm: any; /** @ngInject */ constructor( @@ -114,7 +115,7 @@ export class DataSourceEditCtrl { } saveChanges(test) { - if (!this.$scope.editForm.$valid) { + if (!this.editForm.$valid) { return; } diff --git a/public/app/features/plugins/import_list/import_list.html b/public/app/features/plugins/import_list/import_list.html index d6c1bb914ce..acb6654d520 100644 --- a/public/app/features/plugins/import_list/import_list.html +++ b/public/app/features/plugins/import_list/import_list.html @@ -17,17 +17,17 @@ v{{dash.revision}} - Installed v{{dash.installedRevision}} + Imported v{{dash.installedRevision}} diff --git a/public/app/features/plugins/import_list/import_list.ts b/public/app/features/plugins/import_list/import_list.ts index 62145623fa2..dd0a8eef524 100644 --- a/public/app/features/plugins/import_list/import_list.ts +++ b/public/app/features/plugins/import_list/import_list.ts @@ -7,11 +7,12 @@ import coreModule from 'app/core/core_module'; export class DashImportListCtrl { dashboards: any[]; plugin: any; + datasource: any; constructor(private $http, private backendSrv, private $rootScope) { this.dashboards = []; - backendSrv.get(`/api/plugins/dashboards/${this.plugin.id}`).then(dashboards => { + backendSrv.get(`/api/plugins/${this.plugin.id}/dashboards`).then(dashboards => { this.dashboards = dashboards; }); } @@ -21,10 +22,19 @@ export class DashImportListCtrl { pluginId: this.plugin.id, path: dash.path, reinstall: reinstall, - inputs: {} + inputs: [] }; - this.backendSrv.post(`/api/plugins/dashboards/install`, installCmd).then(res => { + if (this.datasource) { + installCmd.inputs.push({ + name: '*', + type: 'datasource', + pluginId: this.datasource.type, + value: this.datasource.name + }); + } + + this.backendSrv.post(`/api/dashboards/import`, installCmd).then(res => { this.$rootScope.appEvent('alert-success', ['Dashboard Installed', dash.title]); _.extend(dash, res); }); @@ -46,7 +56,8 @@ export function dashboardImportList() { bindToController: true, controllerAs: 'ctrl', scope: { - plugin: "=" + plugin: "=", + datasource: "=" } }; } diff --git a/public/app/features/plugins/partials/ds_edit.html b/public/app/features/plugins/partials/ds_edit.html index 46177d998cc..3305bfd67af 100644 --- a/public/app/features/plugins/partials/ds_edit.html +++ b/public/app/features/plugins/partials/ds_edit.html @@ -25,7 +25,7 @@
-
+
Name @@ -52,12 +52,12 @@ -
-
Testing....
-
Test results
-
-
{{testing.title}}
-
+
+
Testing....
+
Test results
+
+
{{ctrl.testing.title}}
+
@@ -74,7 +74,7 @@
- +
diff --git a/public/app/features/plugins/partials/plugin_edit.html b/public/app/features/plugins/partials/plugin_edit.html index c3881381ed5..6ae4ac5f95c 100644 --- a/public/app/features/plugins/partials/plugin_edit.html +++ b/public/app/features/plugins/partials/plugin_edit.html @@ -1,7 +1,4 @@ - - {{ctrl.model.name}} -
@@ -23,7 +20,7 @@
-
-
- -
-
- -
-
-
-
- -
+
+ + +
+
+
+ +
+
diff --git a/public/app/features/plugins/plugin_edit_ctrl.ts b/public/app/features/plugins/plugin_edit_ctrl.ts index 2d4362835d4..8e9dc32eeee 100644 --- a/public/app/features/plugins/plugin_edit_ctrl.ts +++ b/public/app/features/plugins/plugin_edit_ctrl.ts @@ -11,6 +11,8 @@ export class PluginEditCtrl { readmeHtml: any; includedDatasources: any; tabIndex: number; + tabs: any; + hasDashboards: any; preUpdateHook: () => any; postUpdateHook: () => any; @@ -19,6 +21,7 @@ export class PluginEditCtrl { this.model = {}; this.pluginId = $routeParams.pluginId; this.tabIndex = 0; + this.tabs = ['Overview']; } init() { @@ -35,15 +38,24 @@ export class PluginEditCtrl { return plug; }); + if (this.model.type === 'app') { + this.tabs.push('Config'); + + this.hasDashboards = _.findWhere(result.includes, {type: 'dashboard'}); + if (this.hasDashboards) { + this.tabs.push('Dashboards'); + } + } + return this.initReadme(); }); } initReadme() { - return this.$http.get(this.model.baseUrl + '/readme.md').then(res => { + return this.backendSrv.get(`/api/plugins/${this.pluginId}/readme`).then(res => { return System.import('remarkable').then(Remarkable => { var md = new Remarkable(); - this.readmeHtml = this.$sce.trustAsHtml(md.render(res.data)); + this.readmeHtml = this.$sce.trustAsHtml(md.render(res)); }); }); } @@ -54,6 +66,7 @@ export class PluginEditCtrl { case 'panel': return 'icon-gf icon-gf-panel'; case 'app': return 'icon-gf icon-gf-apps'; case 'page': return 'icon-gf icon-gf-share'; + case 'dashboard': return 'icon-gf icon-gf-dashboard'; } } @@ -64,9 +77,7 @@ export class PluginEditCtrl { // the next step of execution will block until the promise resolves. // if the promise is rejected, this update will be aborted. if (this.preUpdateHook != null) { - chain = chain.then(function() { - return Promise.resolve(self.preUpdateHook()); - }); + chain = self.preUpdateHook(); } // Perform the core update procedure @@ -78,7 +89,7 @@ export class PluginEditCtrl { secureJsonData: self.model.secureJsonData, }, {}); - return self.backendSrv.post(`/api/org/plugins/${self.pluginId}/settings`, updateCmd); + return self.backendSrv.post(`/api/plugins/${self.pluginId}/settings`, updateCmd); }); // if set, performt he postUpdate hook. If a promise is returned it will block @@ -86,7 +97,7 @@ export class PluginEditCtrl { // resolves. If the promise is rejected the page will not be reloaded. if (this.postUpdateHook != null) { chain = chain.then(function() { - return Promise.resolve(this.postUpdateHook()); + return this.postUpdateHook(); }); } @@ -101,17 +112,16 @@ export class PluginEditCtrl { this.preUpdateHook = callback; } - setPOstUpdateHook(callback: () => any) { + setPostUpdateHook(callback: () => any) { this.postUpdateHook = callback; } - toggleEnabled() { + enable() { + this.model.enabled = true; + this.model.pinned = true; this.update(); } - togglePinned() { - this.update(); - } } angular.module('grafana.controllers').controller('PluginEditCtrl', PluginEditCtrl); diff --git a/public/app/features/plugins/plugin_list_ctrl.ts b/public/app/features/plugins/plugin_list_ctrl.ts index 2693a4d8755..9e9b679c6c1 100644 --- a/public/app/features/plugins/plugin_list_ctrl.ts +++ b/public/app/features/plugins/plugin_list_ctrl.ts @@ -4,11 +4,28 @@ import angular from 'angular'; export class PluginListCtrl { plugins: any[]; + tabIndex: number; /** @ngInject */ - constructor(private backendSrv: any) { + constructor(private backendSrv: any, $location) { + this.tabIndex = 0; - this.backendSrv.get('api/plugins', {embedded: 0}).then(plugins => { + var pluginType = $location.search().type || 'panel'; + switch (pluginType) { + case "datasource": { + this.tabIndex = 1; + break; + } + case "app": { + this.tabIndex = 2; + break; + } + case "panel": + default: + this.tabIndex = 0; + } + + this.backendSrv.get('api/plugins', {embedded: 0, type: pluginType}).then(plugins => { this.plugins = plugins; }); } diff --git a/public/app/features/plugins/plugin_page_ctrl.ts b/public/app/features/plugins/plugin_page_ctrl.ts index 9157f14202e..6a840717a80 100644 --- a/public/app/features/plugins/plugin_page_ctrl.ts +++ b/public/app/features/plugins/plugin_page_ctrl.ts @@ -11,7 +11,7 @@ export class AppPageCtrl { /** @ngInject */ constructor(private backendSrv, private $routeParams: any, private $rootScope) { this.pluginId = $routeParams.pluginId; - this.backendSrv.get(`/api/org/plugins/${this.pluginId}/settings`).then(app => { + this.backendSrv.get(`/api/plugins/${this.pluginId}/settings`).then(app => { this.appModel = app; this.page = _.findWhere(app.pages, {slug: this.$routeParams.slug}); if (!this.page) { diff --git a/public/app/features/templating/templateSrv.js b/public/app/features/templating/templateSrv.js index 3527c386220..0aa0990896c 100644 --- a/public/app/features/templating/templateSrv.js +++ b/public/app/features/templating/templateSrv.js @@ -43,6 +43,9 @@ function (angular, _) { } this.formatValue = function(value, format, variable) { + // for some scopedVars there is no variable + variable = variable || {}; + if (typeof format === 'function') { return format(value, variable, this.formatValue); } @@ -66,6 +69,9 @@ function (angular, _) { return '(' + quotedValues.join(' OR ') + ')'; } case "pipe": { + if (typeof value === 'string') { + return value; + } return value.join('|'); } default: { @@ -126,9 +132,6 @@ function (angular, _) { return target.replace(this._regex, function(match, g1, g2) { variable = self._index[g1 || g2]; - if (!variable) { - return match; - } if (scopedVars) { value = scopedVars[g1 || g2]; @@ -137,6 +140,10 @@ function (angular, _) { } } + if (!variable) { + return match; + } + systemValue = self._grafanaVariables[variable.current.value]; if (systemValue) { return self.formatValue(systemValue, format, variable); diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index 704cbd41e72..a034134e657 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/public/app/plugins/datasource/cloudwatch/datasource.js @@ -222,7 +222,7 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { .map(function(reservations) { return _.pluck(reservations.Instances, targetAttributeName); }) - .flatten().value(); + .flatten().uniq().sortBy().value(); return transformSuggestData(attributes); }); } diff --git a/public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png b/public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png new file mode 100644 index 00000000000..f11f4034751 Binary files /dev/null and b/public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png differ diff --git a/public/app/plugins/datasource/cloudwatch/plugin.json b/public/app/plugins/datasource/cloudwatch/plugin.json index 49c5341bd21..eb2f97335da 100644 --- a/public/app/plugins/datasource/cloudwatch/plugin.json +++ b/public/app/plugins/datasource/cloudwatch/plugin.json @@ -4,5 +4,16 @@ "id": "cloudwatch", "metrics": true, - "annotations": true + "annotations": true, + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + }, + "logos": { + "small": "img/amazon-web-services.png", + "large": "img/amazon-web-services.png" + } + } } diff --git a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html index 5c1b538cf9a..3ddbfed3f80 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html +++ b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html @@ -66,7 +66,7 @@
  • - +
  • diff --git a/public/app/plugins/datasource/elasticsearch/plugin.json b/public/app/plugins/datasource/elasticsearch/plugin.json index 3cd345e5cdf..d4b07ac8e00 100644 --- a/public/app/plugins/datasource/elasticsearch/plugin.json +++ b/public/app/plugins/datasource/elasticsearch/plugin.json @@ -6,7 +6,7 @@ "info": { "description": "Elasticsearch Data Source for Grafana", "author": { - "name": "Grafana Core", + "name": "Grafana Project", "url": "http://grafana.org" }, "keywords": ["elasticsearch"], diff --git a/public/app/plugins/datasource/graphite/dashboards/carbon_metrics.json b/public/app/plugins/datasource/graphite/dashboards/carbon_metrics.json new file mode 100644 index 00000000000..845fc1930bd --- /dev/null +++ b/public/app/plugins/datasource/graphite/dashboards/carbon_metrics.json @@ -0,0 +1,176 @@ +{ + "__inputs": [ + { + "name": "DS_NAME", + "type": "datasource", + "pluginId": "graphite" + } + ], + + "revision": "1.0", + "title": "Graphite Carbon Metrics", + "tags": ["graphite", "carbon"], + "style": "dark", + "timezone": "browser", + "editable": true, + "hideControls": false, + "sharedCrosshair": false, + "rows": [ + { + "collapsable": true, + "collapse": false, + "editable": true, + "height": "350px", + "notice": false, + "panels": [ + { + "aliasColors": {}, + "annotate": { + "enable": false + }, + "bars": false, + "datasource": "${DS_NAME}", + "editable": true, + "fill": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "max": null, + "min": 0, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "loadingEditor": false, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "resolution": 100, + "scale": 1, + "seriesOverrides": [ + { + "alias": "Points Per Update", + "yaxis": 2 + }, + { + "alias": "CPU", + "yaxis": 2 + } + ], + "span": 12, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "alias(sumSeries(carbon.agents.*.updateOperations),\"Updates\") " + }, + { + "refId": "B", + "target": "alias(sumSeries(carbon.agents.*.metricsReceived),'Metrics Received')" + }, + { + "refId": "C", + "target": "alias(sumSeries(carbon.agents.*.committedPoints),'Committed Points')" + }, + { + "refId": "D", + "target": "alias(sumSeries(carbon.agents.*.pointsPerUpdate),'Points Per Update')" + }, + { + "refId": "E", + "target": "alias(averageSeries(carbon.agents.*.cpuUsage),'CPU')" + }, + { + "refId": "F", + "target": "alias(sumSeries(carbon.agents.*.creates),'Creates')" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Graphite Carbon Metrics", + "tooltip": { + "query_as_alias": true, + "shared": false, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ], + "zerofill": true + } + ], + "title": "Row1" + } + ], + "time": { + "from": "now-3h", + "to": "now" + }, + "timepicker": { + "collapse": false, + "enable": true, + "notice": false, + "now": true, + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "status": "Stable", + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "type": "timepicker" + }, + "templating": { + "enable": false, + "list": [] + }, + "annotations": { + "enable": false, + "list": [] + }, + "refresh": false, + "schemaVersion": 8, + "version": 2, + "links": [] +} diff --git a/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json b/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json deleted file mode 100644 index ad2c32be759..00000000000 --- a/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "__inputs": { - "graphite": { - "type": "datasource", - "description": "Graphite datasource" - } - }, - - "title": "Carbon Cache Stats", - "version": 1, - "rows": [ - { - "panels": [ - { - "type": "graph", - "datasource": "__$graphite" - } - ] - } - ] -} diff --git a/public/app/plugins/datasource/graphite/img/graphite_logo.png b/public/app/plugins/datasource/graphite/img/graphite_logo.png new file mode 100644 index 00000000000..e70b40720c4 Binary files /dev/null and b/public/app/plugins/datasource/graphite/img/graphite_logo.png differ diff --git a/public/app/plugins/datasource/graphite/plugin.json b/public/app/plugins/datasource/graphite/plugin.json index f7715f9c5b9..c47c49e05fa 100644 --- a/public/app/plugins/datasource/graphite/plugin.json +++ b/public/app/plugins/datasource/graphite/plugin.json @@ -4,9 +4,20 @@ "id": "graphite", "includes": [ - {"type": "dashboard", "name": "Carbon Cache Stats", "path": "dashboards/carbon_stats.json"} + {"type": "dashboard", "name": "Graphite Carbon Metrics", "path": "dashboards/carbon_metrics.json"} ], "metrics": true, - "annotations": true -} + "annotations": true, + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + }, + "logos": { + "small": "img/graphite_logo.png", + "large": "img/graphite_logo.png" + } + } +} \ No newline at end of file diff --git a/public/app/plugins/datasource/influxdb/img/influxdb_logo.svg b/public/app/plugins/datasource/influxdb/img/influxdb_logo.svg new file mode 100644 index 00000000000..6fad144f8c7 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/img/influxdb_logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/app/plugins/datasource/influxdb/plugin.json b/public/app/plugins/datasource/influxdb/plugin.json index dd60a16b715..605ce168831 100644 --- a/public/app/plugins/datasource/influxdb/plugin.json +++ b/public/app/plugins/datasource/influxdb/plugin.json @@ -5,5 +5,16 @@ "defaultMatchFormat": "regex values", "metrics": true, - "annotations": true + "annotations": true, + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + }, + "logos": { + "small": "img/influxdb_logo.svg", + "large": "img/influxdb_logo.svg" + } + } } diff --git a/public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png b/public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png new file mode 100644 index 00000000000..519d56a476c Binary files /dev/null and b/public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png differ diff --git a/public/app/plugins/datasource/opentsdb/plugin.json b/public/app/plugins/datasource/opentsdb/plugin.json index 333e9d07536..02ba02e6e89 100644 --- a/public/app/plugins/datasource/opentsdb/plugin.json +++ b/public/app/plugins/datasource/opentsdb/plugin.json @@ -5,5 +5,16 @@ "metrics": true, "defaultMatchFormat": "pipe", - "annotations": true + "annotations": true, + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + }, + "logos": { + "small": "img/opentsdb_logo.png", + "large": "img/opentsdb_logo.png" + } + } } diff --git a/public/app/plugins/datasource/prometheus/dashboards/prometheus_stats.json b/public/app/plugins/datasource/prometheus/dashboards/prometheus_stats.json new file mode 100644 index 00000000000..e008317a96e --- /dev/null +++ b/public/app/plugins/datasource/prometheus/dashboards/prometheus_stats.json @@ -0,0 +1,577 @@ +{ + "__inputs": [ + { + "name": "DS_NAME", + "type": "datasource", + "pluginId": "prometheus" + } + ], + + "revision": "1.0", + "title": "Prometheus Stats", + "tags": ["prometheus"], + "style": "dark", + "timezone": "browser", + "editable": true, + "hideControls": true, + "sharedCrosshair": false, + "rows": [ + { + "collapse": false, + "editable": true, + "height": 178, + "panels": [ + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "${DS_NAME}", + "decimals": 1, + "editable": true, + "error": false, + "format": "s", + "id": 5, + "interval": null, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "span": 3, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "targets": [ + { + "expr": "(time() - process_start_time_seconds{job=\"prometheus\"})", + "intervalFactor": 2, + "refId": "A" + } + ], + "thresholds": "", + "title": "Uptime", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "${DS_NAME}", + "editable": true, + "error": false, + "format": "none", + "id": 6, + "interval": null, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "span": 3, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "targets": [ + { + "expr": "prometheus_local_storage_memory_series", + "intervalFactor": 2, + "refId": "A" + } + ], + "thresholds": "0,1,5", + "title": "Local Storage Memory Series", + "type": "singlestat", + "valueFontSize": "70%", + "valueMaps": [], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "${DS_NAME}", + "editable": true, + "error": false, + "format": "none", + "id": 7, + "interval": null, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "span": 3, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "targets": [ + { + "expr": "prometheus_local_storage_indexing_queue_length", + "intervalFactor": 2, + "refId": "A" + } + ], + "thresholds": "0,500,4000", + "title": "Interal Storage Queue Length", + "type": "singlestat", + "valueFontSize": "70%", + "valueMaps": [ + { + "op": "=", + "text": "Empty", + "value": "0" + } + ], + "valueName": "current" + }, + { + "content": "\"Prometheus\nPrometheus\n\n

    You're using Prometheus, an open-source systems monitoring and alerting toolkit originally built at SoundCloud. For more information, check out the Grafana and Prometheus projects.

    ", + "editable": true, + "error": false, + "id": 9, + "links": [], + "mode": "html", + "span": 3, + "style": {}, + "title": "", + "transparent": true, + "type": "text" + } + ], + "title": "New row" + }, + { + "collapse": false, + "editable": true, + "height": 227, + "panels": [ + { + "aliasColors": { + "prometheus": "#C15C17", + "{instance=\"localhost:9090\",job=\"prometheus\"}": "#C15C17" + }, + "bars": false, + "datasource": "${DS_NAME}", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 3, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 9, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(prometheus_local_storage_ingested_samples_total[5m])", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{job}}", + "metric": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Samples ingested (rate-5m)", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + }, + { + "content": "#### Samples Ingested\nThis graph displays the count of samples ingested by the Prometheus server, as measured over the last 5 minutes, per time series in the range vector. When troubleshooting an issue on IRC or Github, this is often the first stat requested by the Prometheus team. ", + "editable": true, + "error": false, + "id": 8, + "links": [], + "mode": "markdown", + "span": 2.995914043583536, + "style": {}, + "title": "", + "transparent": true, + "type": "text" + } + ], + "title": "New row" + }, + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": { + "prometheus": "#F9BA8F", + "{instance=\"localhost:9090\",interval=\"5s\",job=\"prometheus\"}": "#F9BA8F" + }, + "bars": false, + "datasource": "${DS_NAME}", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 5, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(prometheus_target_interval_length_seconds_count[5m])", + "intervalFactor": 2, + "legendFormat": "{{job}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Target Scrapes (last 5m)", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + }, + { + "aliasColors": {}, + "bars": false, + "datasource": "${DS_NAME}", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 14, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "prometheus_target_interval_length_seconds{quantile!=\"0.01\", quantile!=\"0.05\"}", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{quantile}} ({{interval}})", + "metric": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Scrape Duration", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + }, + { + "content": "#### Scrapes\nPrometheus scrapes metrics from instrumented jobs, either directly or via an intermediary push gateway for short-lived jobs. Target scrapes will show how frequently targets are scraped, as measured over the last 5 minutes, per time series in the range vector. Scrape Duration will show how long the scrapes are taking, with percentiles available as series. ", + "editable": true, + "error": false, + "id": 11, + "links": [], + "mode": "markdown", + "span": 3, + "style": {}, + "title": "", + "transparent": true, + "type": "text" + } + ], + "title": "New row" + }, + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": "${DS_NAME}", + "decimals": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 12, + "leftYAxisLabel": "", + "legend": { + "alignAsTable": false, + "avg": false, + "current": false, + "hideEmpty": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 9, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "prometheus_evaluator_duration_milliseconds{quantile!=\"0.01\", quantile!=\"0.05\"}", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{quantile}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Rule Eval Duration", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "percentunit", + "short" + ] + }, + { + "content": "#### Rule Evaluation Duration\nThis graph panel plots the duration for all evaluations to execute. The 50th percentile, 90th percentile and 99th percentile are shown as three separate series to help identify outliers that may be skewing the data.", + "editable": true, + "error": false, + "id": 15, + "links": [], + "mode": "markdown", + "span": 3, + "style": {}, + "title": "", + "transparent": true, + "type": "text" + } + ], + "title": "New row" + } + ], + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": { + "now": true, + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "templating": { + "list": [] + }, + "annotations": { + "list": [] + }, + "refresh": false, + "schemaVersion": 7, + "links": [ + { + "icon": "info", + "tags": [], + "targetBlank": true, + "title": "Grafana Docs", + "tooltip": "", + "type": "link", + "url": "http://www.grafana.org/docs" + }, + { + "icon": "info", + "tags": [], + "targetBlank": true, + "title": "Prometheus Docs", + "type": "link", + "url": "http://prometheus.io/docs/introduction/overview/" + } + ] +} diff --git a/public/app/plugins/datasource/prometheus/img/prometheus_logo.svg b/public/app/plugins/datasource/prometheus/img/prometheus_logo.svg new file mode 100644 index 00000000000..29005ec3860 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/img/prometheus_logo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/public/app/plugins/datasource/prometheus/plugin.json b/public/app/plugins/datasource/prometheus/plugin.json index 66dd9145e6a..f39f8691661 100644 --- a/public/app/plugins/datasource/prometheus/plugin.json +++ b/public/app/plugins/datasource/prometheus/plugin.json @@ -3,6 +3,21 @@ "name": "Prometheus", "id": "prometheus", + "includes": [ + {"type": "dashboard", "name": "Prometheus Stats", "path": "dashboards/prometheus_stats.json"} + ], + "metrics": true, - "annotations": true + "annotations": true, + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + }, + "logos": { + "small": "img/prometheus_logo.svg", + "large": "img/prometheus_logo.svg" + } + } } diff --git a/public/app/plugins/panel/dashlist/module.ts b/public/app/plugins/panel/dashlist/module.ts index c0e34183548..7d3f9d74bd0 100644 --- a/public/app/plugins/panel/dashlist/module.ts +++ b/public/app/plugins/panel/dashlist/module.ts @@ -43,13 +43,10 @@ class DashListCtrl extends PanelCtrl { var params: any = {limit: this.panel.limit}; if (this.panel.mode === 'recently viewed') { - var dashboardIds = impressions.getDashboardOpened(); + var dashIds = _.first(impressions.getDashboardOpened(), this.panel.limit); - return this.backendSrv.search({ - dashboardIds: impressions.getDashboardOpened(), - limit: this.panel.limit - }).then(result => { - this.dashList = dashboardIds.map(orderId => { + return this.backendSrv.search({dashboardIds: dashIds, limit: this.panel.limit}).then(result => { + this.dashList = dashIds.map(orderId => { return _.find(result, dashboard => { return dashboard.id === orderId; }); diff --git a/public/app/plugins/panel/dashlist/plugin.json b/public/app/plugins/panel/dashlist/plugin.json index 1f1266d8444..9cac424ac68 100644 --- a/public/app/plugins/panel/dashlist/plugin.json +++ b/public/app/plugins/panel/dashlist/plugin.json @@ -1,5 +1,12 @@ { "type": "panel", "name": "Dashboard list", - "id": "dashlist" + "id": "dashlist", + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + } + } } diff --git a/public/app/plugins/panel/graph/plugin.json b/public/app/plugins/panel/graph/plugin.json index f603e5e4b06..baa9fe12a39 100644 --- a/public/app/plugins/panel/graph/plugin.json +++ b/public/app/plugins/panel/graph/plugin.json @@ -1,5 +1,12 @@ { "type": "panel", "name": "Graph", - "id": "graph" + "id": "graph", + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + } + } } diff --git a/public/app/plugins/panel/singlestat/plugin.json b/public/app/plugins/panel/singlestat/plugin.json index 8372e4b2c2d..ea9ae32853a 100644 --- a/public/app/plugins/panel/singlestat/plugin.json +++ b/public/app/plugins/panel/singlestat/plugin.json @@ -1,5 +1,12 @@ { "type": "panel", "name": "Singlestat", - "id": "singlestat" + "id": "singlestat", + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + } + } } diff --git a/public/app/plugins/panel/table/plugin.json b/public/app/plugins/panel/table/plugin.json index 7f8c0bb23cf..e25efdfbe36 100644 --- a/public/app/plugins/panel/table/plugin.json +++ b/public/app/plugins/panel/table/plugin.json @@ -1,5 +1,12 @@ { "type": "panel", "name": "Table", - "id": "table" + "id": "table", + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + } + } } diff --git a/public/app/plugins/panel/text/plugin.json b/public/app/plugins/panel/text/plugin.json index 4cf046cec36..485f42942f2 100644 --- a/public/app/plugins/panel/text/plugin.json +++ b/public/app/plugins/panel/text/plugin.json @@ -1,5 +1,12 @@ { "type": "panel", "name": "Text", - "id": "text" + "id": "text", + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + } + } } diff --git a/public/fonts/opensans/59ZRklaO5bWGqF5A9baEERJtnKITppOI_IvcXXDNrsc.woff2 b/public/fonts/opensans/59ZRklaO5bWGqF5A9baEERJtnKITppOI_IvcXXDNrsc.woff2 new file mode 100644 index 00000000000..bb845920dd1 Binary files /dev/null and b/public/fonts/opensans/59ZRklaO5bWGqF5A9baEERJtnKITppOI_IvcXXDNrsc.woff2 differ diff --git a/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTRWV49_lSm1NYrwo-zkhivY.woff2 b/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTRWV49_lSm1NYrwo-zkhivY.woff2 new file mode 100644 index 00000000000..04a10815758 Binary files /dev/null and b/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTRWV49_lSm1NYrwo-zkhivY.woff2 differ diff --git a/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTT0LW-43aMEzIO6XUTLjad8.woff2 b/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTT0LW-43aMEzIO6XUTLjad8.woff2 new file mode 100644 index 00000000000..3e198c19ab8 Binary files /dev/null and b/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTT0LW-43aMEzIO6XUTLjad8.woff2 differ diff --git a/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTZX5f-9o1vgP2EXwfjgl7AY.woff2 b/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTZX5f-9o1vgP2EXwfjgl7AY.woff2 new file mode 100644 index 00000000000..853563940a0 Binary files /dev/null and b/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTZX5f-9o1vgP2EXwfjgl7AY.woff2 differ diff --git a/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTa-j2U0lmluP9RWlSytm3ho.woff2 b/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTa-j2U0lmluP9RWlSytm3ho.woff2 new file mode 100644 index 00000000000..acb92ca3e91 Binary files /dev/null and b/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTa-j2U0lmluP9RWlSytm3ho.woff2 differ diff --git a/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTaaRobkAwv3vxw3jMhVENGA.woff2 b/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTaaRobkAwv3vxw3jMhVENGA.woff2 new file mode 100644 index 00000000000..ab00dd46ede Binary files /dev/null and b/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTaaRobkAwv3vxw3jMhVENGA.woff2 differ diff --git a/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTegdm0LZdjqr5-oayXSOefg.woff2 b/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTegdm0LZdjqr5-oayXSOefg.woff2 new file mode 100644 index 00000000000..96ae7edd3e4 Binary files /dev/null and b/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTegdm0LZdjqr5-oayXSOefg.woff2 differ diff --git a/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTf8zf_FOSsgRmwsS7Aa9k2w.woff2 b/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTf8zf_FOSsgRmwsS7Aa9k2w.woff2 new file mode 100644 index 00000000000..cd40e6ac9e7 Binary files /dev/null and b/public/fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTf8zf_FOSsgRmwsS7Aa9k2w.woff2 differ diff --git a/public/fonts/opensans/K88pR3goAWT7BTt32Z01mxJtnKITppOI_IvcXXDNrsc.woff2 b/public/fonts/opensans/K88pR3goAWT7BTt32Z01mxJtnKITppOI_IvcXXDNrsc.woff2 new file mode 100644 index 00000000000..0f1eeae6cc2 Binary files /dev/null and b/public/fonts/opensans/K88pR3goAWT7BTt32Z01mxJtnKITppOI_IvcXXDNrsc.woff2 differ diff --git a/public/fonts/opensans/LWCjsQkB6EMdfHrEVqA1KRJtnKITppOI_IvcXXDNrsc.woff2 b/public/fonts/opensans/LWCjsQkB6EMdfHrEVqA1KRJtnKITppOI_IvcXXDNrsc.woff2 new file mode 100644 index 00000000000..cd471286244 Binary files /dev/null and b/public/fonts/opensans/LWCjsQkB6EMdfHrEVqA1KRJtnKITppOI_IvcXXDNrsc.woff2 differ diff --git a/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNShWV49_lSm1NYrwo-zkhivY.woff2 b/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNShWV49_lSm1NYrwo-zkhivY.woff2 new file mode 100644 index 00000000000..97af2592f21 Binary files /dev/null and b/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNShWV49_lSm1NYrwo-zkhivY.woff2 differ diff --git a/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSj0LW-43aMEzIO6XUTLjad8.woff2 b/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSj0LW-43aMEzIO6XUTLjad8.woff2 new file mode 100644 index 00000000000..bb98ecd5169 Binary files /dev/null and b/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSj0LW-43aMEzIO6XUTLjad8.woff2 differ diff --git a/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSpX5f-9o1vgP2EXwfjgl7AY.woff2 b/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSpX5f-9o1vgP2EXwfjgl7AY.woff2 new file mode 100644 index 00000000000..d539aeb5ea8 Binary files /dev/null and b/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSpX5f-9o1vgP2EXwfjgl7AY.woff2 differ diff --git a/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSq-j2U0lmluP9RWlSytm3ho.woff2 b/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSq-j2U0lmluP9RWlSytm3ho.woff2 new file mode 100644 index 00000000000..7019bcecdec Binary files /dev/null and b/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSq-j2U0lmluP9RWlSytm3ho.woff2 differ diff --git a/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSqaRobkAwv3vxw3jMhVENGA.woff2 b/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSqaRobkAwv3vxw3jMhVENGA.woff2 new file mode 100644 index 00000000000..4111b36671a Binary files /dev/null and b/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSqaRobkAwv3vxw3jMhVENGA.woff2 differ diff --git a/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSugdm0LZdjqr5-oayXSOefg.woff2 b/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSugdm0LZdjqr5-oayXSOefg.woff2 new file mode 100644 index 00000000000..8993a4de6b3 Binary files /dev/null and b/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSugdm0LZdjqr5-oayXSOefg.woff2 differ diff --git a/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSv8zf_FOSsgRmwsS7Aa9k2w.woff2 b/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSv8zf_FOSsgRmwsS7Aa9k2w.woff2 new file mode 100644 index 00000000000..363eedbf59d Binary files /dev/null and b/public/fonts/opensans/MTP_ySUJH_bn48VBG8sNSv8zf_FOSsgRmwsS7Aa9k2w.woff2 differ diff --git a/public/fonts/opensans/RjgO7rYTmqiVp7vzi-Q5URJtnKITppOI_IvcXXDNrsc.woff2 b/public/fonts/opensans/RjgO7rYTmqiVp7vzi-Q5URJtnKITppOI_IvcXXDNrsc.woff2 new file mode 100644 index 00000000000..1fd235c2d3d Binary files /dev/null and b/public/fonts/opensans/RjgO7rYTmqiVp7vzi-Q5URJtnKITppOI_IvcXXDNrsc.woff2 differ diff --git a/public/fonts/opensans/cJZKeOuBrn4kERxqtaUH3VtXRa8TVwTICgirnJhmVJw.woff2 b/public/fonts/opensans/cJZKeOuBrn4kERxqtaUH3VtXRa8TVwTICgirnJhmVJw.woff2 new file mode 100644 index 00000000000..5287058cf90 Binary files /dev/null and b/public/fonts/opensans/cJZKeOuBrn4kERxqtaUH3VtXRa8TVwTICgirnJhmVJw.woff2 differ diff --git a/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzBWV49_lSm1NYrwo-zkhivY.woff2 b/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzBWV49_lSm1NYrwo-zkhivY.woff2 new file mode 100644 index 00000000000..1503cf25927 Binary files /dev/null and b/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzBWV49_lSm1NYrwo-zkhivY.woff2 differ diff --git a/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzD0LW-43aMEzIO6XUTLjad8.woff2 b/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzD0LW-43aMEzIO6XUTLjad8.woff2 new file mode 100644 index 00000000000..4d8eb897568 Binary files /dev/null and b/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzD0LW-43aMEzIO6XUTLjad8.woff2 differ diff --git a/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzJX5f-9o1vgP2EXwfjgl7AY.woff2 b/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzJX5f-9o1vgP2EXwfjgl7AY.woff2 new file mode 100644 index 00000000000..8394e2ec8a3 Binary files /dev/null and b/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzJX5f-9o1vgP2EXwfjgl7AY.woff2 differ diff --git a/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzK-j2U0lmluP9RWlSytm3ho.woff2 b/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzK-j2U0lmluP9RWlSytm3ho.woff2 new file mode 100644 index 00000000000..bef0033fff8 Binary files /dev/null and b/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzK-j2U0lmluP9RWlSytm3ho.woff2 differ diff --git a/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzKaRobkAwv3vxw3jMhVENGA.woff2 b/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzKaRobkAwv3vxw3jMhVENGA.woff2 new file mode 100644 index 00000000000..ffc6ce4387d Binary files /dev/null and b/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzKaRobkAwv3vxw3jMhVENGA.woff2 differ diff --git a/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzOgdm0LZdjqr5-oayXSOefg.woff2 b/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzOgdm0LZdjqr5-oayXSOefg.woff2 new file mode 100644 index 00000000000..be4c25b4299 Binary files /dev/null and b/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzOgdm0LZdjqr5-oayXSOefg.woff2 differ diff --git a/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzP8zf_FOSsgRmwsS7Aa9k2w.woff2 b/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzP8zf_FOSsgRmwsS7Aa9k2w.woff2 new file mode 100644 index 00000000000..cf04c330b9d Binary files /dev/null and b/public/fonts/opensans/k3k702ZOKiLJc3WVjuplzP8zf_FOSsgRmwsS7Aa9k2w.woff2 differ diff --git a/public/fonts/opensans/u-WUoqrET9fUeobQW7jkRRJtnKITppOI_IvcXXDNrsc.woff2 b/public/fonts/opensans/u-WUoqrET9fUeobQW7jkRRJtnKITppOI_IvcXXDNrsc.woff2 new file mode 100644 index 00000000000..0b5a66eb4ca Binary files /dev/null and b/public/fonts/opensans/u-WUoqrET9fUeobQW7jkRRJtnKITppOI_IvcXXDNrsc.woff2 differ diff --git a/public/fonts/opensans/xjAJXh38I15wypJXxuGMBiYE0-AqJ3nfInTTiDXDjU4.woff2 b/public/fonts/opensans/xjAJXh38I15wypJXxuGMBiYE0-AqJ3nfInTTiDXDjU4.woff2 new file mode 100644 index 00000000000..72208f35fbd Binary files /dev/null and b/public/fonts/opensans/xjAJXh38I15wypJXxuGMBiYE0-AqJ3nfInTTiDXDjU4.woff2 differ diff --git a/public/fonts/opensans/xjAJXh38I15wypJXxuGMBjTOQ_MqJVwkKsUn0wKzc2I.woff2 b/public/fonts/opensans/xjAJXh38I15wypJXxuGMBjTOQ_MqJVwkKsUn0wKzc2I.woff2 new file mode 100644 index 00000000000..6b18d55dce1 Binary files /dev/null and b/public/fonts/opensans/xjAJXh38I15wypJXxuGMBjTOQ_MqJVwkKsUn0wKzc2I.woff2 differ diff --git a/public/fonts/opensans/xjAJXh38I15wypJXxuGMBjUj_cnvWIuuBMVgbX098Mw.woff2 b/public/fonts/opensans/xjAJXh38I15wypJXxuGMBjUj_cnvWIuuBMVgbX098Mw.woff2 new file mode 100644 index 00000000000..03b166f8bf4 Binary files /dev/null and b/public/fonts/opensans/xjAJXh38I15wypJXxuGMBjUj_cnvWIuuBMVgbX098Mw.woff2 differ diff --git a/public/fonts/opensans/xjAJXh38I15wypJXxuGMBkbcKLIaa1LC45dFaAfauRA.woff2 b/public/fonts/opensans/xjAJXh38I15wypJXxuGMBkbcKLIaa1LC45dFaAfauRA.woff2 new file mode 100644 index 00000000000..fc8efef1395 Binary files /dev/null and b/public/fonts/opensans/xjAJXh38I15wypJXxuGMBkbcKLIaa1LC45dFaAfauRA.woff2 differ diff --git a/public/fonts/opensans/xjAJXh38I15wypJXxuGMBmo_sUJ8uO4YLWRInS22T3Y.woff2 b/public/fonts/opensans/xjAJXh38I15wypJXxuGMBmo_sUJ8uO4YLWRInS22T3Y.woff2 new file mode 100644 index 00000000000..61d2a4e5495 Binary files /dev/null and b/public/fonts/opensans/xjAJXh38I15wypJXxuGMBmo_sUJ8uO4YLWRInS22T3Y.woff2 differ diff --git a/public/fonts/opensans/xjAJXh38I15wypJXxuGMBo4P5ICox8Kq3LLUNMylGO4.woff2 b/public/fonts/opensans/xjAJXh38I15wypJXxuGMBo4P5ICox8Kq3LLUNMylGO4.woff2 new file mode 100644 index 00000000000..a739e6ab026 Binary files /dev/null and b/public/fonts/opensans/xjAJXh38I15wypJXxuGMBo4P5ICox8Kq3LLUNMylGO4.woff2 differ diff --git a/public/fonts/opensans/xjAJXh38I15wypJXxuGMBr6up8jxqWt8HVA3mDhkV_0.woff2 b/public/fonts/opensans/xjAJXh38I15wypJXxuGMBr6up8jxqWt8HVA3mDhkV_0.woff2 new file mode 100644 index 00000000000..002c5777e23 Binary files /dev/null and b/public/fonts/opensans/xjAJXh38I15wypJXxuGMBr6up8jxqWt8HVA3mDhkV_0.woff2 differ diff --git a/public/fonts/opensans/xozscpT2726on7jbcb_pAhJtnKITppOI_IvcXXDNrsc.woff2 b/public/fonts/opensans/xozscpT2726on7jbcb_pAhJtnKITppOI_IvcXXDNrsc.woff2 new file mode 100644 index 00000000000..7971f534272 Binary files /dev/null and b/public/fonts/opensans/xozscpT2726on7jbcb_pAhJtnKITppOI_IvcXXDNrsc.woff2 differ diff --git a/public/img/plugin-default-logo_dark.svg b/public/img/plugin-default-logo_dark.svg new file mode 100644 index 00000000000..d665aa30b07 --- /dev/null +++ b/public/img/plugin-default-logo_dark.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/plugin-default-logo_light.svg b/public/img/plugin-default-logo_light.svg new file mode 100644 index 00000000000..4573494ff16 --- /dev/null +++ b/public/img/plugin-default-logo_light.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 42956f3c3fa..8ee73d537e3 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -14,7 +14,6 @@ @import "base/reboot"; @import "base/type"; @import "base/forms"; -@import "base/fonts"; @import "base/grid"; @import "base/font_awesome"; @@ -30,6 +29,7 @@ @import "layout/page"; // COMPONENTS +@import "components/cards"; @import "components/buttons"; @import "components/navs"; @import "components/tabs"; diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 4cabda83901..8efd9f7e55d 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -67,9 +67,9 @@ $link-hover-color: $white; // Typography // ------------------------- -$headings-color: darken($white,11%); -$abbr-border-color: $gray-3 !default; -$text-muted: darken($link-color,30%); +$headings-color: darken($white,11%); +$abbr-border-color: $gray-3 !default; +$text-muted: darken($link-color,30%); $blockquote-small-color: $gray-3 !default; $blockquote-border-color: $gray-4 !default; @@ -113,8 +113,8 @@ $scrollbarBorder: black; // Tables // ------------------------- $table-bg: transparent; // overall background-color -$table-bg-accent: rgba(100, 100, 100, 0.3); // for striping -$table-bg-hover: $dark-3; // for hover +$table-bg-accent: $dark-3; // for striping +$table-bg-hover: $dark-4; // for hover $table-border: $dark-3; // table and cell border // Buttons @@ -252,3 +252,10 @@ $popover-help-color: $text-color; // images $checkboxImageUrl: '../img/checkbox.png'; + +// cards +$card-background: linear-gradient(135deg, #2f2f2f, #262626); +$card-background-hover: linear-gradient(135deg, #343434, #262626); +$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .3); + + diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index af66e2c2389..279e284b0be 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -279,3 +279,9 @@ $popover-help-color: $gray-6; // images $checkboxImageUrl: '../img/checkbox_white.png'; + +// cards +$card-background: linear-gradient(135deg, $gray-5, $gray-6); +$card-background-hover: linear-gradient(135deg, $gray-6, $gray-7); +$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .1); + diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss index 4f1002bdc67..9f96591a4b9 100644 --- a/public/sass/_variables.scss +++ b/public/sass/_variables.scss @@ -4,7 +4,7 @@ // // Quickly modify global styling by enabling or disabling optional features. -$enable-flex: false !default; +$enable-flex: true !default; $enable-rounded: true !default; $enable-shadows: false !default; $enable-gradients: false !default; diff --git a/public/sass/base/_fonts.scss b/public/sass/base/_fonts.scss index f7d172a523f..fb339bc60cf 100644 --- a/public/sass/base/_fonts.scss +++ b/public/sass/base/_fonts.scss @@ -1,178 +1,280 @@ +/* cyrillic-ext */ @font-face { - font-family: 'grafana-icons'; - src: url('../fonts/grafana-icons.eot?h6rv8b'); - src: url('../fonts/grafana-icons.eot?h6rv8b#iefix') format('embedded-opentype'), - url('../fonts/grafana-icons.ttf?h6rv8b') format('truetype'), - url('../fonts/grafana-icons.woff?h6rv8b') format('woff'), - url('../fonts/grafana-icons.svg?h6rv8b#grafana-icons') format('svg'); - font-weight: normal; - font-style: normal; + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + src: local('Open Sans Light'), local('OpenSans-Light'), url(../fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTa-j2U0lmluP9RWlSytm3ho.woff2) format('woff2'); + unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; } - -.icon-gf { - /* use !important to prevent issues with browser extensions that change fonts */ - font-family: 'grafana-icons' !important; - speak: none; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - - /* Better Font Rendering =========== */ - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; +/* cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + src: local('Open Sans Light'), local('OpenSans-Light'), url(../fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTZX5f-9o1vgP2EXwfjgl7AY.woff2) format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } - -.inline-icon-gf { - vertical-align: middle; +/* greek-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + src: local('Open Sans Light'), local('OpenSans-Light'), url(../fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTRWV49_lSm1NYrwo-zkhivY.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; } - -.icon-gf-raintank_wordmark:before { - content: "\e600"; +/* greek */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + src: local('Open Sans Light'), local('OpenSans-Light'), url(../fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTaaRobkAwv3vxw3jMhVENGA.woff2) format('woff2'); + unicode-range: U+0370-03FF; } -.icon-gf-raintank_icn:before { - content: "\e601"; +/* vietnamese */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + src: local('Open Sans Light'), local('OpenSans-Light'), url(../fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTf8zf_FOSsgRmwsS7Aa9k2w.woff2) format('woff2'); + unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB; } -.icon-gf-raintank_r-icn:before { - content: "\e905"; +/* latin-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + src: local('Open Sans Light'), local('OpenSans-Light'), url(../fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTT0LW-43aMEzIO6XUTLjad8.woff2) format('woff2'); + unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; } -.icon-gf-check-alt:before { - content: "\e603"; +/* latin */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + src: local('Open Sans Light'), local('OpenSans-Light'), url(../fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTegdm0LZdjqr5-oayXSOefg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; } -.icon-gf-check:before { - content: "\e604"; +/* cyrillic-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + src: local('Open Sans'), local('OpenSans'), url(../fonts/opensans/K88pR3goAWT7BTt32Z01mxJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); + unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; } -.icon-gf-collector:before { - content: "\e605"; +/* cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + src: local('Open Sans'), local('OpenSans'), url(../fonts/opensans/RjgO7rYTmqiVp7vzi-Q5URJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } -.icon-gf-dashboard:before { - content: "\e606"; +/* greek-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + src: local('Open Sans'), local('OpenSans'), url(../fonts/opensans/LWCjsQkB6EMdfHrEVqA1KRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; } -.icon-gf-panel:before { - content: "\e904"; +/* greek */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + src: local('Open Sans'), local('OpenSans'), url(../fonts/opensans/xozscpT2726on7jbcb_pAhJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); + unicode-range: U+0370-03FF; } -.icon-gf-datasources:before { - content: "\e607"; +/* vietnamese */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + src: local('Open Sans'), local('OpenSans'), url(../fonts/opensans/59ZRklaO5bWGqF5A9baEERJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); + unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB; } -.icon-gf-endpoint-tiny:before { - content: "\e608"; +/* latin-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + src: local('Open Sans'), local('OpenSans'), url(../fonts/opensans/u-WUoqrET9fUeobQW7jkRRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); + unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; } -.icon-gf-endpoint:before { - content: "\e609"; +/* latin */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + src: local('Open Sans'), local('OpenSans'), url(../fonts/opensans/cJZKeOuBrn4kERxqtaUH3VtXRa8TVwTICgirnJhmVJw.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; } -.icon-gf-filter:before { - content: "\e60a"; +/* cyrillic-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url(../fonts/opensans/MTP_ySUJH_bn48VBG8sNSq-j2U0lmluP9RWlSytm3ho.woff2) format('woff2'); + unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; } -.icon-gf-status:before { - content: "\e60b"; +/* cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url(../fonts/opensans/MTP_ySUJH_bn48VBG8sNSpX5f-9o1vgP2EXwfjgl7AY.woff2) format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } -.icon-gf-monitoring:before { - content: "\e60c"; +/* greek-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url(../fonts/opensans/MTP_ySUJH_bn48VBG8sNShWV49_lSm1NYrwo-zkhivY.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; } -.icon-gf-monitoring-tiny:before { - content: "\e620"; +/* greek */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url(../fonts/opensans/MTP_ySUJH_bn48VBG8sNSqaRobkAwv3vxw3jMhVENGA.woff2) format('woff2'); + unicode-range: U+0370-03FF; } -.icon-gf-jump-to-dashboard:before { - content: "\e60d"; +/* vietnamese */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url(../fonts/opensans/MTP_ySUJH_bn48VBG8sNSv8zf_FOSsgRmwsS7Aa9k2w.woff2) format('woff2'); + unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB; } -.icon-gf-warn:before { - content: "\e60e"; +/* latin-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url(../fonts/opensans/MTP_ySUJH_bn48VBG8sNSj0LW-43aMEzIO6XUTLjad8.woff2) format('woff2'); + unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; } -.icon-gf-nodata:before { - content: "\e60f"; +/* latin */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url(../fonts/opensans/MTP_ySUJH_bn48VBG8sNSugdm0LZdjqr5-oayXSOefg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; } -.icon-gf-critical:before { - content: "\e610"; +/* cyrillic-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 700; + src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/opensans/k3k702ZOKiLJc3WVjuplzK-j2U0lmluP9RWlSytm3ho.woff2) format('woff2'); + unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; } -.icon-gf-online:before { - content: "\e611"; +/* cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 700; + src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/opensans/k3k702ZOKiLJc3WVjuplzJX5f-9o1vgP2EXwfjgl7AY.woff2) format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } -.icon-gf-event-error:before { - content: "\e623"; +/* greek-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 700; + src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/opensans/k3k702ZOKiLJc3WVjuplzBWV49_lSm1NYrwo-zkhivY.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; } -.icon-gf-event:before { - content: "\e624"; +/* greek */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 700; + src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/opensans/k3k702ZOKiLJc3WVjuplzKaRobkAwv3vxw3jMhVENGA.woff2) format('woff2'); + unicode-range: U+0370-03FF; } -.icon-gf-sadface:before { - content: "\e907"; +/* vietnamese */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 700; + src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/opensans/k3k702ZOKiLJc3WVjuplzP8zf_FOSsgRmwsS7Aa9k2w.woff2) format('woff2'); + unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB; } -.icon-gf-private-collector:before { - content: "\e612"; +/* latin-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 700; + src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/opensans/k3k702ZOKiLJc3WVjuplzD0LW-43aMEzIO6XUTLjad8.woff2) format('woff2'); + unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; } -.icon-gf-alert:before { - content: "\e61f"; +/* latin */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 700; + src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/opensans/k3k702ZOKiLJc3WVjuplzOgdm0LZdjqr5-oayXSOefg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; } -.icon-gf-alert-disabled:before { - content: "\e621"; +/* cyrillic-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/opensans/xjAJXh38I15wypJXxuGMBjTOQ_MqJVwkKsUn0wKzc2I.woff2) format('woff2'); + unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; } -.icon-gf-refresh:before { - content: "\e613"; +/* cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/opensans/xjAJXh38I15wypJXxuGMBjUj_cnvWIuuBMVgbX098Mw.woff2) format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } -.icon-gf-save:before { - content: "\e614"; +/* greek-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/opensans/xjAJXh38I15wypJXxuGMBkbcKLIaa1LC45dFaAfauRA.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; } -.icon-gf-settings:before { - content: "\e615"; +/* greek */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/opensans/xjAJXh38I15wypJXxuGMBmo_sUJ8uO4YLWRInS22T3Y.woff2) format('woff2'); + unicode-range: U+0370-03FF; } -.icon-gf-share:before { - content: "\e616"; +/* vietnamese */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/opensans/xjAJXh38I15wypJXxuGMBr6up8jxqWt8HVA3mDhkV_0.woff2) format('woff2'); + unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB; } -.icon-gf-star:before { - content: "\e617"; +/* latin-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/opensans/xjAJXh38I15wypJXxuGMBiYE0-AqJ3nfInTTiDXDjU4.woff2) format('woff2'); + unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; } -.icon-gf-search:before { - content: "\e618"; -} -.icon-gf-tag-add:before { - content: "\e619"; -} -.icon-gf-tag-remove:before { - content: "\e61a"; -} -.icon-gf-video:before { - content: "\e61b"; -} -.icon-gf-bulk_action:before { - content: "\e61c"; -} -.icon-gf-users:before { - content: "\e622"; -} -.icon-gf-globe:before { - content: "\e61d"; -} -.icon-gf-snapshot:before { - content: "\e61e"; -} -.icon-gf-play-grafana-icon:before { - content: "\e629"; -} -.icon-gf-grafana-icon:before { - content: "\e625"; -} -.icon-gf-email:before { - content: "\e628"; -} -.icon-gf-stopwatch:before { - content: "\e626"; -} -.icon-gf-skull:before { - content: "\e900"; -} -.icon-gf-probe:before { - content: "\e901"; -} -.icon-gf-apps:before { - content: "\e902"; -} -.icon-gf-scale:before { - content: "\e906"; -} -.icon-gf-litmus:before { - content: "\e627"; -} -.icon-gf-grafana_wordmark:before { - content: "\e903"; -} - +/* latin */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/opensans/xjAJXh38I15wypJXxuGMBo4P5ICox8Kq3LLUNMylGO4.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; +} \ No newline at end of file diff --git a/public/sass/base/_grafana_icons.scss b/public/sass/base/_grafana_icons.scss new file mode 100644 index 00000000000..e442cbdce6c --- /dev/null +++ b/public/sass/base/_grafana_icons.scss @@ -0,0 +1,185 @@ +@font-face { + font-family: 'grafana-icons'; + src: url('../fonts/grafana-icons.eot?h6rv8b'); + src: url('../fonts/grafana-icons.eot?h6rv8b#iefix') format('embedded-opentype'), + url('../fonts/grafana-icons.ttf?h6rv8b') format('truetype'), + url('../fonts/grafana-icons.woff?h6rv8b') format('woff'), + url('../fonts/grafana-icons.svg?h6rv8b#grafana-icons') format('svg'); + font-weight: normal; + font-style: normal; +} + +.icon-gf { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'grafana-icons' !important; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.inline-icon-gf { + vertical-align: middle; +} + +.icon-gf-raintank_wordmark:before { + content: "\e600"; +} +.icon-gf-raintank_icn:before { + content: "\e601"; +} +.icon-gf-raintank_r-icn:before { + content: "\e905"; +} +.icon-gf-check-alt:before { + content: "\e603"; +} +.icon-gf-check:before { + content: "\e604"; +} +.icon-gf-collector:before { + content: "\e605"; +} +.icon-gf-dashboard:before { + content: "\e606"; +} +.icon-gf-panel:before { + content: "\e904"; +} +.icon-gf-datasources:before { + content: "\e607"; +} +.icon-gf-endpoint-tiny:before { + content: "\e608"; +} +.icon-gf-endpoint:before { + content: "\e609"; +} +.icon-gf-filter:before { + content: "\e60a"; +} +.icon-gf-status:before { + content: "\e60b"; +} +.icon-gf-monitoring:before { + content: "\e60c"; +} +.icon-gf-monitoring-tiny:before { + content: "\e620"; +} +.icon-gf-jump-to-dashboard:before { + content: "\e60d"; +} +.icon-gf-warn:before { + content: "\e60e"; +} +.icon-gf-nodata:before { + content: "\e60f"; +} +.icon-gf-critical:before { + content: "\e610"; +} +.icon-gf-online:before { + content: "\e611"; +} +.icon-gf-event-error:before { + content: "\e623"; +} +.icon-gf-event:before { + content: "\e624"; +} +.icon-gf-sadface:before { + content: "\e907"; +} +.icon-gf-private-collector:before { + content: "\e612"; +} +.icon-gf-alert:before { + content: "\e61f"; +} +.icon-gf-alert-disabled:before { + content: "\e621"; +} +.icon-gf-refresh:before { + content: "\e613"; +} +.icon-gf-save:before { + content: "\e614"; +} +.icon-gf-settings:before { + content: "\e615"; +} +.icon-gf-share:before { + content: "\e616"; +} +.icon-gf-star:before { + content: "\e617"; +} +.icon-gf-search:before { + content: "\e618"; +} +.icon-gf-tag-add:before { + content: "\e619"; +} +.icon-gf-tag-remove:before { + content: "\e61a"; +} +.icon-gf-video:before { + content: "\e61b"; +} +.icon-gf-bulk_action:before { + content: "\e61c"; +} +.icon-gf-users:before { + content: "\e622"; +} +.icon-gf-globe:before { + content: "\e61d"; +} +.icon-gf-snapshot:before { + content: "\e61e"; +} +.icon-gf-play-grafana-icon:before { + content: "\e629"; +} +.icon-gf-grafana-icon:before { + content: "\e625"; +} +.icon-gf-email:before { + content: "\e628"; +} +.icon-gf-stopwatch:before { + content: "\e626"; +} +.icon-gf-skull:before { + content: "\e900"; +} +.icon-gf-probe:before { + content: "\e901"; +} +.icon-gf-apps:before { + content: "\e902"; +} +.icon-gf-scale:before { + content: "\e906"; +} +.icon-gf-litmus:before { + content: "\e627"; +} +.icon-gf-grafana_wordmark:before { + content: "\e903"; +} + +.icon-gf-app:before { + content: "\e902"; +} +.icon-gf-datasource:before { + content: "\e607"; +} + diff --git a/public/sass/components/_cards.scss b/public/sass/components/_cards.scss new file mode 100644 index 00000000000..8272de2fa3b --- /dev/null +++ b/public/sass/components/_cards.scss @@ -0,0 +1,133 @@ +.layout-selector { + text-align: right; + margin: 0 $spacer*1.5 $spacer; + position: relative; + top: -0.5rem; + + button { + background: $input-label-bg; + color: $text-color-weak; + border: none; + padding: 0.5rem; + line-height: 1; + font-size: 130%; + + &:focus { + outline: none; + } + + &.active { + background-color: lighten($input-label-bg, 5%); + color: $link-color; + } + } +} + +.card-section { + margin-bottom: $spacer*2; +} + +.card-list { + display: flex; + flex-direction: row; + flex-wrap: wrap; + list-style-type: none; +} + +.card-item { + display: block; + height: 100%; + background: $card-background; + box-shadow: $card-shadow; + padding: 1rem; + border-radius: 4px; + + &:hover { + background: $card-background-hover; + } +} + +.card-item-body { + display: flex; +} + +.card-item-details { +} + +.card-item-header { + color: $text-color-weak; + margin-bottom: $spacer; +} + +.card-item-name { + color: $headings-color; +} + +.card-item-sub-name { + color: $text-color-weak; +} + +.card-list-layout-grid { + + .card-item-wrapper { + width: 100%; + padding: 0 1.5rem 1.5rem 0rem; + } + + .card-item-figure { + margin: 0 $spacer $spacer 0; + height: 6rem; + + img { + width: 6rem; + } + } + + .card-item-name { + font-size: $font-size-h3; + } + + @include media-breakpoint-up(md) { + .card-item-wrapper { + width: 50%; + } + } + + @include media-breakpoint-up(lg) { + .card-item-wrapper { + width: 33.333333%; + } + } +} + +.card-list-layout-list { + + .card-item-wrapper { + width: 100%; + padding: 0 0 1.5rem 0rem; + } + + .card-item-header { + float: right; + } + + .card-item-figure { + margin: 0 $spacer 0 0; + img { + width: 3.5rem; + } + } + + .card-item-name { + font-size: $font-size-h4; + } + + .card-item-sub-name { + font-size: $font-size-sm; + } + + .layout-selector { + margin-right: 0; + } +} + diff --git a/public/sass/fonts.scss b/public/sass/fonts.scss new file mode 100644 index 00000000000..c45e63111c3 --- /dev/null +++ b/public/sass/fonts.scss @@ -0,0 +1,3 @@ +@import "base/fonts"; +@import "base/font_awesome"; +@import "base/grafana_icons"; diff --git a/public/sass/pages/_plugins.scss b/public/sass/pages/_plugins.scss index 1464c2ecf56..970d048cce3 100644 --- a/public/sass/pages/_plugins.scss +++ b/public/sass/pages/_plugins.scss @@ -30,6 +30,7 @@ img { width: 16px; } + white-space: nowrap; max-width: $page-sidebar-width; text-overflow: ellipsis; @@ -40,8 +41,25 @@ img { max-width: 100%; } + ul { padding-left: $spacer*1.5; + margin-bottom: $spacer*2; + } + + table { + td, th { + padding: $spacer*.5 $spacer; + } + th { + font-weight: normal; + background: $table-bg-accent; + } + } + + table, th, td { + border: 1px solid $table-border; + border-collapse: collapse; } } diff --git a/public/views/index.html b/public/views/index.html index 3208c5ae9ff..49bfe2c5bb9 100644 --- a/public/views/index.html +++ b/public/views/index.html @@ -8,7 +8,7 @@ Grafana - + [[if .User.LightTheme]] diff --git a/tasks/default_task.js b/tasks/default_task.js index 4ce68409c45..d2bebb89789 100644 --- a/tasks/default_task.js +++ b/tasks/default_task.js @@ -6,6 +6,7 @@ module.exports = function(grunt) { 'sass', 'concat:cssDark', 'concat:cssLight', + 'concat:cssFonts', 'styleguide', 'sasslint', 'postcss' diff --git a/tasks/options/concat.js b/tasks/options/concat.js index 59042fc26b0..a5e0753b0cd 100644 --- a/tasks/options/concat.js +++ b/tasks/options/concat.js @@ -22,6 +22,11 @@ module.exports = function(config) { dest: '<%= genDir %>/css/grafana.light.min.css' }, + cssFonts: { + src: [ '<%= genDir %>/css/fonts.css' ], + dest: '<%= genDir %>/css/fonts.min.css' + }, + js: { src: [ '<%= genDir %>/vendor/npm/es5-shim/es5-shim.js', diff --git a/tasks/options/sass.js b/tasks/options/sass.js index f96970740a0..baec4cd861d 100644 --- a/tasks/options/sass.js +++ b/tasks/options/sass.js @@ -6,6 +6,7 @@ module.exports = function(config) { files: { "<%= genDir %>/css/grafana.dark.css": "<%= srcDir %>/sass/grafana.dark.scss", "<%= genDir %>/css/grafana.light.css": "<%= srcDir %>/sass/grafana.light.scss", + "<%= genDir %>/css/fonts.css": "<%= srcDir %>/sass/fonts.scss", } } }; diff --git a/tests/test-app/dashboards/connections.json b/tests/test-app/dashboards/connections.json index f1f62826f73..ee5e40140b3 100644 --- a/tests/test-app/dashboards/connections.json +++ b/tests/test-app/dashboards/connections.json @@ -1,5 +1,28 @@ { + "__inputs": [ + { + "name": "DS_NAME", + "type": "datasource", + "pluginId": "graphite" + } + ], + "title": "Nginx Connections", "revision": "1.5", - "schemaVersion": 11 + "schemaVersion": 11, + "tags": ["tag1", "tag2"], + "number_array": [1,2,3,10.33], + "boolean_true": true, + "boolean_false": false, + + "rows": [ + { + "panels": [ + { + "type": "graph", + "datasource": "${DS_NAME}" + } + ] + } + ] } diff --git a/tests/test-app/dashboards/connections_result.json b/tests/test-app/dashboards/connections_result.json new file mode 100644 index 00000000000..1c662f6c269 --- /dev/null +++ b/tests/test-app/dashboards/connections_result.json @@ -0,0 +1,20 @@ +{ + "revision": "1.5", + "tags": ["tag1", "tag2"], + "boolean_false": false, + "boolean_true": true, + "number_array": [1,2,3,10.33], + "rows": [ + { + "panels": [ + { + "datasource": "graphite", + "type": "graph" + } + ] + } + ], + "schemaVersion": 11, + "title": "Nginx Connections", + "version": 0 +}