diff --git a/.editorconfig b/.editorconfig index 5760be58369..831bb8696cc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,13 @@ # http://editorconfig.org root = true +[*.go] +indent_style = tabs +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + [*] indent_style = space indent_size = 2 diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000000..61f039fe615 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,12 @@ +Thank you! For helping us make Grafana even better. + +To help us respond to your issues faster, please make sure to add as much information as possible. + +If this issue is about a plugin, please open the issue in that repository. + +Start your issues title with [Feature Request] / [Bug] / [Question] or no tag if your unsure. + +Ex +* What grafana version are you using? +* What datasource are you using? +* What OS are you running grafana on? diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..9eacff7c5de --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,2 @@ +* Link the PR to an issue for new features +* Rebase your PR if it gets out of sync with master \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index bd9b6c0ec6d..40a0421d45a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ * **graph**: Template variables can now be used in TimeShift and TimeFrom, closes[#1960](https://github.com/grafana/grafana/issues/1960) * **Tooltip**: Optionally add milliseconds to timestamp in tool tip, closes[#2248](https://github.com/grafana/grafana/issues/2248) * **Opentsdb**: Support milliseconds when using openTSDB datasource, closes [#2865](https://github.com/grafana/grafana/issues/2865) +* **Opentsdb**: Add support for annotations, closes[#664](https://github.com/grafana/grafana/issues/664) ### Bug fixes * **Playlist**: Fix for memory leak when running a playlist, closes [#3794](https://github.com/grafana/grafana/pull/3794) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index b75fdfb574a..bc1cb68bb5b 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -64,6 +64,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", @@ -123,10 +128,6 @@ "Comment": "v0.4.4-44-gf561133", "Rev": "f56113384f2c63dfe4cd8e768e349f1c35122b58" }, - { - "ImportPath": "github.com/gopherjs/gopherjs/js", - "Rev": "14d893dca2e4adb93a5ccc9494040acc0821cd8d" - }, { "ImportPath": "github.com/gosimple/slug", "Rev": "8d258463b4459f161f51d6a357edacd3eef9d663" @@ -160,6 +161,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/mkdocs.yml b/docs/mkdocs.yml index e88b5ec4237..b527fa8f046 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -25,9 +25,10 @@ google_analytics: ['UA-47280256-1', 'grafana.org'] pages: # Introduction: -- ['index.md', 'Project', 'About Grafana'] -- ['project/cla.md', 'Project', 'Contributor License Agreement'] +# - ['index.md', 'Project', 'About Grafana'] +# - ['project/cla.md', 'Project', 'Contributor License Agreement'] +- ['index.md', '**HIDDEN**'] - ['installation/index.md', 'Installation', 'Installation'] - ['installation/debian.md', 'Installation', 'Installing on Debian / Ubuntu'] - ['installation/rpm.md', 'Installation', 'Installing on RPM-based Linux'] @@ -62,7 +63,6 @@ pages: - ['reference/templating.md', 'Reference', 'Templating'] - ['reference/scripting.md', 'Reference', 'Scripting'] - ['reference/playlist.md', 'Reference', 'Playlist'] -- ['reference/plugins.md', 'Reference', 'Plugins'] - ['reference/export_import.md', 'Reference', 'Import & Export'] - ['reference/admin.md', 'Reference', 'Administration'] - ['reference/keyboard_shortcuts.md', 'Reference', 'Keyboard Shortcuts'] @@ -90,6 +90,8 @@ pages: - ['plugins/installation.md', 'Plugins', 'Installation'] - ['plugins/datasources.md', 'Plugins', 'Datasource plugins'] - ['plugins/panels.md', 'Plugins', 'Panel plugins'] +- ['plugins/development.md', 'Plugins', 'Plugin development'] +- ['plugins/plugin.json.md', 'Plugins', 'Plugin json'] - ['tutorials/index.md', 'Tutorials', 'Tutorials'] - ['tutorials/hubot_howto.md', 'Tutorials', 'How To integrate Hubot and Grafana'] diff --git a/docs/sources/datasources/cloudwatch.md b/docs/sources/datasources/cloudwatch.md index 4d9d8b38c1c..351b08eb2aa 100644 --- a/docs/sources/datasources/cloudwatch.md +++ b/docs/sources/datasources/cloudwatch.md @@ -64,9 +64,19 @@ Name | Description `metrics(namespace)` | Returns a list of metrics in the namespace. `dimension_keys(namespace)` | Returns a list of dimension keys in the namespace. `dimension_values(region, namespace, metric, dimension_key)` | Returns a list of dimension values matching the specified `region`, `namespace`, `metric` and `dimension_key`. +`ebs_volume_ids(region, instance_id)` | Returns a list of volume id matching the specified `region`, `instance_id`. +`ec2_instance_attribute(region, attribute_name, filters)` | Returns a list of attribute matching the specified `region`, `attribute_name`, `filters`. For details about the metrics CloudWatch provides, please refer to the [CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html). +The `ec2_instance_attribute` query take `filters` in JSON format. +You can specify [pre-defined filters of ec2:DescribeInstances](http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html). +Specify like `{ filter_name1: [ filter_value1 ], filter_name2: [ filter_value2 ] }` + +Example `ec2_instance_attribute()` query + + ec2_instance_attribute(us-east-1, InstanceId, { "tag:Environment": [ "production" ] }) + ![](/img/v2/cloudwatch_templating.png) ## Cost diff --git a/docs/sources/http_api/data_source.md b/docs/sources/http_api/data_source.md index 57352161d31..2a3c20e9af8 100644 --- a/docs/sources/http_api/data_source.md +++ b/docs/sources/http_api/data_source.md @@ -74,6 +74,59 @@ page_keywords: grafana, admin, http, api, documentation, datasource "jsonData":null } +## Get a single data source by Name + +`GET /api/datasources/name/:name` + +**Example Request**: + + GET /api/datasources/name/test_datasource HTTP/1.1 + Accept: application/json + Content-Type: application/json + Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +**Example Response**: + + HTTP/1.1 200 + Content-Type: application/json + + { + "id":1, + "orgId":1, + "name":"test_datasource", + "type":"graphite", + "access":"proxy", + "url":"http://mydatasource.com", + "password":"", + "user":"", + "database":"", + "basicAuth":false, + "basicAuthUser":"", + "basicAuthPassword":"", + "isDefault":false, + "jsonData":null + } + +## Get data source Id by Name + +`GET /api/datasources/id/:name` + +**Example Request**: + + GET /api/datasources/id/test_datasource HTTP/1.1 + Accept: application/json + Content-Type: application/json + Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +**Example Response**: + + HTTP/1.1 200 + Content-Type: application/json + + { + "id":1 + } + ## Create data source `POST /api/datasources` diff --git a/docs/sources/index.md b/docs/sources/index.md index f595c81ef28..83b7f44dd5b 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -1,39 +1,37 @@ -page_title: About Grafana -page_description: Introduction to Grafana. -page_keywords: grafana, introduction, documentation, about +--- +page_title: Grafana Installation +page_description: Install guide for Grafana. +page_keywords: grafana, installation, documentation +--- -# About Grafana +# Installation -Grafana is a leading open source application for visualizing large-scale measurement data. +Grafana is easily installed via a Debian/Ubuntu package (.deb), via +Redhat/Centos package (.rpm) or manually via a tarball that contains all +required files and binaries. If you can't find a package or binary for +your platform, you might be able to build one yourself. Read the [build +from source](../project/building_from_source) instructions for more +information. -It provides a powerful and elegant way to create, share, and explore data and dashboards from your disparate metric databases, either with your team or the world. +## Platforms +- [Installing on Debian / Ubuntu](installation/debian.md) +- [Installing on RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)](installation/rpm.md) +- [Installing on Mac OS X](installation/mac.md) +- [Installing on Windows](installation/windows.md) +- [Installing on Docker](installation/docker.md) +- [Installing using Provisioning (Chef, Puppet, Salt, Ansible, etc)](installation/provisioning.md) +- [Nightly Builds](http://grafana.org/download/builds.html) -Grafana is most commonly used for Internet infrastructure and application analytics, but many use it in other domains including industrial sensors, home automation, weather, and process control. +## Configuration -Grafana features pluggable panels and data sources allowing easy extensibility. There is currently rich support for [Graphite](http://graphite.readthedocs.org/en/latest/), [InfluxDB](http://influxdb.org) and [OpenTSDB](http://opentsdb.net). There is also experimental support for [KairosDB](https://github.com/kairosdb/kairosdb), [Prometheus](http://prometheus.io/), and SQL is on the roadmap. Grafana has a variety of panels, including a fully featured graph panel with rich visualization options. +The back-end web server has a number of configuration options. Go the +[Configuration](/installation/configuration) page for details on all +those options. -Version 2.0 was released in April 2015: Grafana now ships with its own backend server that brings [many changes and features](../guides/whats-new-in-v2/). -Version 2.1 was released in July 2015 and added [even more features and enhancements](../guides/whats-new-in-v2-1/). +## Data sources guides -## Community Resources, Feedback, and Support +- [Graphite](datasources/graphite.md) +- [Elasticsearch](datasources/elasticsearch.md) +- [InfluxDB](datasources/influxdb.md) +- [OpenTSDB](datasources/opentsdb.md) -Thousands of organizations large and small rely on Grafana, and we have a vibrant and active community that constantly inspires us. - -Please don't hesitate to [open a new issue on Github](https://github.com/grafana/grafana/issues) with your suggestions, ideas, and bug reports. - -Most of the new features and improvements that go into Grafana come from our users. We greatly value your feedback and suggestions; we consider them paramount to making the product better! - -If you have any trouble with Grafana, whether you can't get it set up or you just want clarification on a feature, there are a number of ways to get help: - -- [Troubleshooting guide](/installation/troubleshooting/) -- \#grafana IRC channel on the freenode network (chat.freenode.net) -- Search closed and open [issues on GitHub](https://github.com/grafana/grafana/issues) -- [Mailing list](https://groups.io/org/groupsio/grafana) - -## Commercial Support - -[raintank](http://www.raintank.io), the company behind Grafana, will be launching a SaaS Grafana-based platform later this year that will also include commercial support for all your existing Grafana installations. Please sign up for [early access at raintank](http://www.raintank.io) for more information. - -## License - -By utilizing this software, you agree to the terms of the included license. Grafana is licensed under the Apache 2.0 agreement. See [LICENSE](https://github.com/grafana/grafana/blob/master/LICENSE.md) for the full license terms. diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index b652abcabb6..cdc24dc7c59 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -159,19 +159,19 @@ The database user's password (not applicable for `sqlite3`). For Postgres, use either `disable`, `require` or `verify-full`. For MySQL, use either `true`, `false`, or `skip-verify`. -### ca_cert_path +### ca_cert_path (MySQL only) The path to the CA certificate to use. On many linux systems, certs can be found in `/etc/ssl/certs`. -### client_key_path +### client_key_path (MySQL only) The path to the client key. Only if server requires client authentication. -### client_cert_path +### client_cert_path (MySQL only) The path to the client cert. Only if server requires client authentication. -### server_cert_name +### server_cert_name (MySQL only) The common name field of the certificate used by the `mysql` server. Not necessary if `ssl_mode` is set to `skip-verify`. @@ -373,7 +373,7 @@ Set to `true` to enable auto sign up of users who do not exist in Grafana DB. De ### provider -Valid values are `memory`, `file`, `mysql`, `postgres`, `memcache`. Default is `file`. +Valid values are `memory`, `file`, `mysql`, `postgres`, `memcache` or `redis`. Default is `file`. ### provider_config @@ -384,6 +384,7 @@ session provider you have configured. - **mysql:** go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name` - **postgres:** ex: user=a password=b host=localhost port=5432 dbname=c sslmode=disable - **memcache:** ex: 127.0.0.1:11211 +- **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,db=grafana` If you use MySQL or Postgres as the session store you need to create the session table manually. @@ -415,10 +416,10 @@ How long sessions lasts in seconds. Defaults to `86400` (24 hours). ### reporting_enabled -When enabled Grafana will send anonymous usage statistics to +When enabled Grafana will send anonymous usage statistics to `stats.grafana.org`. No IP addresses are being tracked, only simple counters to track running instances, versions, dashboard & error counts. It is very helpful -to us, so please leave this enabled. Counters are sent every 24 hours. Default +to us, so please leave this enabled. Counters are sent every 24 hours. Default value is `true`. ### google_analytics_ua_id diff --git a/docs/sources/installation/index.md b/docs/sources/installation/index.md index 21c8c127b46..395c101a8e8 100644 --- a/docs/sources/installation/index.md +++ b/docs/sources/installation/index.md @@ -13,6 +13,7 @@ your platform, you might be able to build one yourself. Read the [build from source](../project/building_from_source) instructions for more information. +## Platforms - [Installing on Debian / Ubuntu](debian.md) - [Installing on RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)](rpm.md) - [Installing on Mac OS X](mac.md) diff --git a/docs/sources/installation/ldap.md b/docs/sources/installation/ldap.md index 82309ec0f19..a5311fb4fa5 100644 --- a/docs/sources/installation/ldap.md +++ b/docs/sources/installation/ldap.md @@ -6,7 +6,7 @@ page_keywords: grafana, ldap, configuration, documentation, integration # LDAP Integration -Grafana 2.1 ships with a strong LDAP integration feature. The LDAP integration in Grafana allows your +Grafana (2.1 and newer) ships with a strong LDAP integration feature. The LDAP integration in Grafana allows your Grafana users to login with their LDAP credentials. You can also specify mappings between LDAP group memberships and Grafana Organization user roles. diff --git a/docs/sources/plugins/datasources.md b/docs/sources/plugins/datasources.md index c33b033ffa3..6bd11242f52 100644 --- a/docs/sources/plugins/datasources.md +++ b/docs/sources/plugins/datasources.md @@ -4,6 +4,168 @@ page_description: Datasource plugins for Grafana page_keywords: grafana, plugins, documentation --- + > Our goal is not to have a very extensive documentation but rather have actual code that people can look at. An example implementation of a datasource can be found in the grafana repo under /examples/datasource-plugin-genericdatasource + # Datasources -TODO \ No newline at end of file +Datasource plugins enables people to develop plugins for any database that communicates over http. Its up to the plugin to transform the data into time series data so that any grafana panel can then show it. + +To interact with the rest of grafana the plugins module file can export 5 different components. + +- Datasource (Required) +- QueryCtrl (Required) +- ConfigCtrl (Required) +- QueryOptionsCtrl +- AnnotationsQueryCtrl + +## Plugin json +There are two datasource specific settings for the plugin.json +```javascript +"metrics": true, +"annotations": false, +``` +These settings indicates what kind of data the plugin can deliver. At least one of them have to be true + +## Datasource +The javascript object that communicates with the database and transforms data to times series. + +The Datasource should contain the following functions. +``` +query(options) //used by panels to get data +testDatasource() //used by datasource configuration page to make sure the connection is working +annotationsQuery(options) // used dashboards to get annotations +metricFindQuery(options) // used by query editor to get metric suggestions. +``` + +### Query + +Request object passed to datasource.query function +```json +{ + "range": { "from": "2015-12-22T03:06:13.851Z", "to": "2015-12-22T06:48:24.137Z" }, + "interval": "5s", + "targets": [ + { "refId": "B", "target": "upper_75" }, + { "refId": "A", "target": "upper_90" } + ], + "format": "json", + "maxDataPoints": 2495 //decided by the panel +} +``` + +There are two different kind of results for datasources. +Time series and table. Time series is the most common format and is supported by all datasources and panels. Table format is only support by the Influxdb datasource and table panel. But we might see more of this in the future. + +Time series response from datasource.query +An array of +```json +[ + { + "target":"upper_75", + "datapoints":[ + [622,1450754160000], + [365,1450754220000] + ] + }, + { + "target":"upper_90", + "datapoints":[ + [861,1450754160000], + [767,1450754220000] + ] + } +] +``` + +Table response from datasource.query +An array of +```json +[ + { + "columns": [ + { + "text": "Time", + "type": "time", + "sort": true, + "desc": true, + }, + { + "text": "mean", + }, + { + "text": "sum", + } + ], + "rows": [ + [ + 1457425380000, + null, + null + ], + [ + 1457425370000, + 1002.76215352, + 1002.76215352 + ], + ], + "type": "table" + } +] +``` + +### Annotation Query + +Request object passed to datasource.annotationsQuery function +```json +{ + "range": { "from": "2016-03-04T04:07:55.144Z", "to": "2016-03-04T07:07:55.144Z" }, + "rangeRaw": { "from": "now-3h", to: "now" }, + "annotation": { + "datasource": "generic datasource", + "enable": true, + "name": "annotation name" + } +} +``` + +Expected result from datasource.annotationQuery +```json +[ + { + "annotation": { + "name": "annotation name", //should match the annotation name in grafana + "enabled": true, + "datasource": "generic datasource", + }, + "title": "Cluster outage", + "time": 1457075272576, + "text": "Joe causes brain split", + "tags": "joe, cluster, failure" + } +] +``` + + +## QueryCtrl + +A javascript class that will be instantiated and treated as an Angular controller when the user edits metrics in a panel. This class have to inherit from the app/plugins/sdk.QueryCtrl class. + +Requires a static template or templateUrl variable which will be rendered as the view for this controller. + +## ConfigCtrl + +A javascript class that will be instantiated and treated as an Angular controller when a user tries to edit or create a new datasource of this type. + +Requires a static template or templateUrl variable which will be rendered as the view for this controller. + +## QueryOptionsCtrl + +A javascript class that will be instantiated and treated as an Angular controller when the user edits metrics in a panel. This controller is responsible for handling panel wide settings for the datasource. Such as interval, rate and aggregations if needed. + +Requires a static template or templateUrl variable which will be rendered as the view for this controller. + +## AnnotationsQueryCtrl + +A javascript class that will be instantiated and treated as an Angular controller when the user choose this type of datasource in the templating menu in the dashboard. + +Requires a static template or templateUrl variable which will be rendered as the view for this controller. The fields that are bound to this controller is then sent to the Database objects annotationsQuery function. diff --git a/docs/sources/plugins/developing_plugins.md b/docs/sources/plugins/developing_plugins.md new file mode 100644 index 00000000000..b235fa8d3de --- /dev/null +++ b/docs/sources/plugins/developing_plugins.md @@ -0,0 +1,30 @@ +--- +page_title: Plugin development +page_description: Plugin development for Grafana +page_keywords: grafana, plugins, documentation, development +--- + +# Plugin development + +From grafana 3.0 it's very easy to develop your own plugins and share them with other grafana users. + +## What languages? + +Since everything turns into javascript its up to you to choose which language you want. That said its proberbly a good idea to choose es6 or typescript since we use es6 classes in Grafana. + +##Buildscript + +You can use any buildsystem you like that support systemjs. All the built content should endup in a folder named dist and commited to the repository. + +##Loading plugins +The easiset way to try your plugin with grafana is to [setup grafana for development](https://github.com/grafana/grafana/blob/master/DEVELOPMENT.md) and place your plugin in the /data/plugins folder in grafana. When grafana starts it will scan that folder for folders that contains a plugin.json file and mount them as plugins. If your plugin folder contains a folder named dist it will mount that folder instead of the plugin base folder. + +## Examples / boilerplate +We currently have three different examples that you can fork to get started developing your grafana plugin. + + - [generic-datasource](https://github.com/grafana/grafana/tree/master/examples/datasource-plugin-genericdatasource) (small datasource plugin for quering json data from backends) + - [panel-boilderplate-es5](https://github.com/grafana/grafana/tree/master/examples/panel-boilerplate-es5) + - [nginx-app](https://github.com/grafana/grafana/tree/master/examples/nginx-app) + +## Publish your plugin +We are currently working on this. diff --git a/docs/sources/plugins/overview.md b/docs/sources/plugins/overview.md index 4863b4ee3f4..6d0864ac8f5 100644 --- a/docs/sources/plugins/overview.md +++ b/docs/sources/plugins/overview.md @@ -6,5 +6,7 @@ page_keywords: grafana, plugins, documentation # Plugins -TODO +From Grafana 3.0 not only datasource plugins are supported but also panel plugins and apps. Having panels as plugins make it easy to create and add any kind of panel, to show your data or improve your favorite dashboards. Apps is something new in Grafana that enables bundling of datasources, panels that belongs together. + +Grafana already have a strong community of contributors and plugin developers. By making it easier to develop and install plugins we hope that the community can grow even stronger and develop new plugins that we would never think about. diff --git a/docs/sources/plugins/panels.md b/docs/sources/plugins/panels.md index a164eeebb35..ad9d5db66d8 100644 --- a/docs/sources/plugins/panels.md +++ b/docs/sources/plugins/panels.md @@ -4,7 +4,26 @@ page_description: Panel plugins for Grafana page_keywords: grafana, plugins, documentation --- + > Our goal is not to have a very extensive documentation but rather have actual code that people can look at. An example implementation of a datasource can be found in the grafana repo under /examples/panel-boilerplate-es5 + # Panels -TODO +To interact with the rest of grafana the panel plugin need to export a class in the module.js. +This class have to inherit from sdk.PanelCtrl or sdk.MetricsPanelCtrl and be exported as PanelCtrl. + +```javascript + return { + PanelCtrl: BoilerPlatePanelCtrl + }; +``` + +This class will be instantiated once for every panel of its kind in a dashboard and treated as an AngularJs controller. + +## MetricsPanelCtrl or PanelCtrl + +MetricsPanelCtrl inherits from PanelCtrl and adds some common features for datasource usage. So if your Panel will be working with a datasource you should inherit from MetricsPanelCtrl. If don't need to access any datasource then you should inherit from PanelCtrl instead. + +## Implementing a MetricsPanelCtrl + +If you choose to inherit from MetricsPanelCtrl you should implement a function called refreshData that will take a datasource as in parameter when its time to get new data. Its recommended that the refreshData function calls the issueQueries in the base class but its not mandatory. An examples of such implementation can be found in our [example panel](https://github.com/grafana/grafana/blob/master/examples/panel-boilerplate-es5/module.js#L27-L38) diff --git a/docs/sources/plugins/plugin.json.md b/docs/sources/plugins/plugin.json.md new file mode 100644 index 00000000000..dd4f2f58560 --- /dev/null +++ b/docs/sources/plugins/plugin.json.md @@ -0,0 +1,10 @@ +--- +page_title: Plugin json file +page_description: Plugin json for Grafana +page_keywords: grafana, plugins, documentation +--- + +# Plugin.json + +TODO + diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000000..64341a7fdbc --- /dev/null +++ b/examples/README.md @@ -0,0 +1,4 @@ +## Example plugin implementations + +datasource:[simple-json-datasource](https://github.com/grafana/simple-json-datasource) +app: [example-app](https://github.com/grafana/example-app) \ No newline at end of file diff --git a/examples/boilerplate-es5-panel/css/styles.css b/examples/boilerplate-es5-panel/css/styles.css new file mode 100644 index 00000000000..b7259cb6cc8 --- /dev/null +++ b/examples/boilerplate-es5-panel/css/styles.css @@ -0,0 +1,3 @@ +.panel-boilerplate-values { + text-align: center; +} \ No newline at end of file diff --git a/examples/boilerplate-es5-panel/module.js b/examples/boilerplate-es5-panel/module.js new file mode 100644 index 00000000000..ef6b23ea749 --- /dev/null +++ b/examples/boilerplate-es5-panel/module.js @@ -0,0 +1,53 @@ +define([ + 'app/plugins/sdk', + 'lodash', + './css/styles.css!' +], function(sdk, _) { + + var BoilerPlatePanelCtrl = (function(_super) { + var self; + + function BoilerPlatePanelCtrl($scope, $injector) { + _super.call(this, $scope, $injector); + + this.results = [] + + self = this; + } + + // you do not need a templateUrl, you can use a inline template here + // BoilerPlatePanelCtrl.template = '

boilerplate

'; + + // all panel static assets can be accessed via 'public/plugins// + BoilerPlatePanelCtrl.templateUrl = 'panel.html'; + + BoilerPlatePanelCtrl.prototype = Object.create(_super.prototype); + BoilerPlatePanelCtrl.prototype.constructor = BoilerPlatePanelCtrl; + + BoilerPlatePanelCtrl.prototype.refreshData = function(datasource) { + this.issueQueries(datasource) + .then(function(result) { + self.results = []; + _.each(result.data, function(target) { + var last = _.last(target.datapoints) + self.results.push(last[0]); + }); + + self.render(); + }); + } + + BoilerPlatePanelCtrl.prototype.render = function() { + this.values = this.results.join(','); + } + + return BoilerPlatePanelCtrl; + + })(sdk.MetricsPanelCtrl); + + + return { + PanelCtrl: BoilerPlatePanelCtrl + }; +}); + diff --git a/examples/boilerplate-es5-panel/panel.html b/examples/boilerplate-es5-panel/panel.html new file mode 100644 index 00000000000..4a413c95dfe --- /dev/null +++ b/examples/boilerplate-es5-panel/panel.html @@ -0,0 +1,7 @@ +

+ Basic panel +

+ +

{{ctrl.values}}

+ + diff --git a/examples/panel-boilerplate-es5/plugin.json b/examples/boilerplate-es5-panel/plugin.json similarity index 100% rename from examples/panel-boilerplate-es5/plugin.json rename to examples/boilerplate-es5-panel/plugin.json diff --git a/examples/datasource-plugin-genericdatasource/.jscs.json b/examples/datasource-plugin-genericdatasource/.jscs.json deleted file mode 100644 index dcf694dcc63..00000000000 --- a/examples/datasource-plugin-genericdatasource/.jscs.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "disallowImplicitTypeConversion": ["string"], - "disallowKeywords": ["with"], - "disallowMultipleLineBreaks": true, - "disallowMixedSpacesAndTabs": true, - "disallowTrailingWhitespace": true, - "requireSpacesInFunctionExpression": { - "beforeOpeningCurlyBrace": true - }, - "disallowSpacesInsideArrayBrackets": true, - "disallowSpacesInsideParentheses": true, - "validateIndentation": 2 -} \ No newline at end of file diff --git a/examples/datasource-plugin-genericdatasource/.jshintrc b/examples/datasource-plugin-genericdatasource/.jshintrc deleted file mode 100644 index 3725af83afc..00000000000 --- a/examples/datasource-plugin-genericdatasource/.jshintrc +++ /dev/null @@ -1,36 +0,0 @@ -{ - "browser": true, - "esnext": true, - - "bitwise":false, - "curly": true, - "eqnull": true, - "devel": true, - "eqeqeq": true, - "forin": false, - "immed": true, - "supernew": true, - "expr": true, - "indent": 2, - "latedef": true, - "newcap": true, - "noarg": true, - "noempty": true, - "undef": true, - "boss": true, - "trailing": true, - "laxbreak": true, - "laxcomma": true, - "sub": true, - "unused": true, - "maxdepth": 6, - "maxlen": 140, - - "globals": { - "System": true, - "define": true, - "require": true, - "Chromath": false, - "setImmediate": true - } -} diff --git a/examples/datasource-plugin-genericdatasource/Gruntfile.js b/examples/datasource-plugin-genericdatasource/Gruntfile.js deleted file mode 100644 index d36a4716f31..00000000000 --- a/examples/datasource-plugin-genericdatasource/Gruntfile.js +++ /dev/null @@ -1,54 +0,0 @@ -module.exports = function(grunt) { - - require('load-grunt-tasks')(grunt); - - grunt.loadNpmTasks('grunt-execute'); - grunt.loadNpmTasks('grunt-contrib-clean'); - - grunt.initConfig({ - - clean: ["dist"], - - copy: { - src_to_dist: { - cwd: 'src', - expand: true, - src: ['**/*', '!**/*.js', '!**/*.scss'], - dest: 'dist' - }, - pluginDef: { - expand: true, - src: 'plugin.json', - dest: 'dist', - } - }, - - watch: { - rebuild_all: { - files: ['src/**/*', 'plugin.json'], - tasks: ['default'], - options: {spawn: false} - }, - }, - - babel: { - options: { - sourceMap: true, - presets: ["es2015"], - plugins: ['transform-es2015-modules-systemjs', "transform-es2015-for-of"], - }, - dist: { - files: [{ - cwd: 'src', - expand: true, - src: ['**/*.js'], - dest: 'dist', - ext:'.js' - }] - }, - }, - - }); - - grunt.registerTask('default', ['clean', 'copy:src_to_dist', 'copy:pluginDef', 'babel']); -}; diff --git a/examples/datasource-plugin-genericdatasource/README.md b/examples/datasource-plugin-genericdatasource/README.md deleted file mode 100644 index 67a9f1fb7ec..00000000000 --- a/examples/datasource-plugin-genericdatasource/README.md +++ /dev/null @@ -1,75 +0,0 @@ -#Generic backend datasource# - -This is a very minimalistic datasource that forwards http requests in a defined format. The idea is that anybody should be able to build an api and retrieve data from any datasource without built-in support in grafana. - -Its also serves as an living example implementation of a datasource. - -A guide for installing plugins can be found at [placeholder for links]. - -Your backend need implement 3 urls - * "/" Should return 200 ok. Used for "Test connection" on the datasource config page. - * "/search" Used by the find metric options on the query tab in panels - * "/query" Should return metrics based on input - -## Metric discovery ## - -### Request ### -``` -{ refId: 'F', target: 'select metric' } -``` -### Expected Response ### - -An array of options based on the target input - -####Example#### -``` -["upper_25","upper_50","upper_75","upper_90","upper_95"] -``` - -## Metric query ## - -### Request ### -``` -{ - range: { from: '2015-12-22T03:06:13.851Z',to: '2015-12-22T06:48:24.137Z' }, - interval: '5s', - targets: - [ { refId: 'B', target: 'upper_75' }, - { refId: 'A', target: 'upper_90' } ], - format: 'json', - maxDataPoints: 2495 //decided by the panel -} -``` -### Expected response ### - -An array of -``` -{ - "target":"target_name", - "datapoints":[ - [intvalue, timestamp in epoch], - [intvalue, timestamp in epoch] - ] -} -``` -###Example### -``` -[ - { - "target":"upper_75", - "datapoints":[ - [622,1450754160000], - [365,1450754220000] - ] - }, - { - "target":"upper_90", - "datapoints":[ - [861,1450754160000], - [767,1450754220000] - ] - } -] -``` -## Example backend implementation ## -https://gist.github.com/bergquist/bc4aa5baface3cffa109 \ No newline at end of file diff --git a/examples/datasource-plugin-genericdatasource/package.json b/examples/datasource-plugin-genericdatasource/package.json deleted file mode 100644 index 91c53734ec8..00000000000 --- a/examples/datasource-plugin-genericdatasource/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "kentik-app", - "private": true, - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/raintank/kentik-app-poc.git" - }, - "author": "", - "license": "ISC", - "bugs": { - "url": "https://github.com/raintank/kentik-app-poc/issues" - }, - "devDependencies": { - "grunt": "~0.4.5", - "babel": "~6.5.1", - "grunt-babel": "~6.0.0", - "grunt-contrib-copy": "~0.8.2", - "grunt-contrib-watch": "^0.6.1", - "grunt-contrib-uglify": "~0.11.0", - "grunt-systemjs-builder": "^0.2.5", - "load-grunt-tasks": "~3.2.0", - "grunt-execute": "~0.2.2", - "grunt-contrib-clean": "~0.6.0" - }, - "dependencies": { - "babel-plugin-transform-es2015-modules-systemjs": "^6.5.0", - "babel-preset-es2015": "^6.5.0", - "lodash": "~4.0.0" - }, - "homepage": "https://github.com/raintank/kentik-app-poc#readme" -} diff --git a/examples/datasource-plugin-genericdatasource/plugin.json b/examples/datasource-plugin-genericdatasource/plugin.json deleted file mode 100644 index 4c3a9accd05..00000000000 --- a/examples/datasource-plugin-genericdatasource/plugin.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "GenericDatasource", - "id": "datasource-plugin-genericdatasource", - "type": "datasource", - - "module": "plugins/genericdatasource/datasource", - - "staticRoot": ".", - - "metrics": true, - "annotations": false, - - "info": { - "description": "generic datsource plugin", - "author": { - "name": "Raintank Inc.", - "url": "http://raintank.io" - }, - "version": "0.9.0", - "updated": "2016-02-10" - }, - - "dependencies": { - "grafanaVersion": "2.6.x", - "plugins": [ ] - } -} diff --git a/examples/datasource-plugin-genericdatasource/src/css/query-editor.css b/examples/datasource-plugin-genericdatasource/src/css/query-editor.css deleted file mode 100644 index 3b678b9f368..00000000000 --- a/examples/datasource-plugin-genericdatasource/src/css/query-editor.css +++ /dev/null @@ -1,3 +0,0 @@ -.generic-datasource-query-row .query-keyword { - width: 75px; -} \ No newline at end of file diff --git a/examples/datasource-plugin-genericdatasource/src/datasource.js b/examples/datasource-plugin-genericdatasource/src/datasource.js deleted file mode 100644 index 1b68cf61fb1..00000000000 --- a/examples/datasource-plugin-genericdatasource/src/datasource.js +++ /dev/null @@ -1,65 +0,0 @@ -export class GenericDatasource { - - constructor(instanceSettings, $q, backendSrv) { - this.type = instanceSettings.type; - this.url = instanceSettings.url; - this.name = instanceSettings.name; - this.q = $q; - this.backendSrv = backendSrv; - } - - // Called once per panel (graph) - query(options) { - var query = this.buildQueryParameters(options); - - if (query.targets.length <= 0) { - return this.q.when([]); - } - - return this.backendSrv.datasourceRequest({ - url: this.url + '/query', - data: query, - method: 'POST', - headers: { 'Content-Type': 'application/json' } - }); - } - - // Required - // Used for testing datasource in datasource configuration pange - testDatasource() { - return this.backendSrv.datasourceRequest({ - url: this.url + '/', - method: 'GET' - }).then(response => { - if (response.status === 200) { - return { status: "success", message: "Data source is working", title: "Success" }; - } - }); - } - - // Optional - // Required for templating - metricFindQuery(options) { - return this.backendSrv.datasourceRequest({ - url: this.url + '/search', - data: options, - method: 'POST', - headers: { 'Content-Type': 'application/json' } - }).then(this.mapToTextValue); - } - - mapToTextValue(result) { - return _.map(result.data, (d, i) => { - return { text: d, value: i}; - }); - } - - buildQueryParameters(options) { - //remove placeholder targets - options.targets = _.filter(options.targets, target => { - return target.target !== 'select metric'; - }); - - return options; - } -} diff --git a/examples/datasource-plugin-genericdatasource/src/module.js b/examples/datasource-plugin-genericdatasource/src/module.js deleted file mode 100644 index 5dc2f9c89d8..00000000000 --- a/examples/datasource-plugin-genericdatasource/src/module.js +++ /dev/null @@ -1,15 +0,0 @@ -import {GenericDatasource} from './datasource'; -import {GenericDatasourceQueryCtrl} from './query_ctrl'; - -class GenericConfigCtrl {} -GenericConfigCtrl.templateUrl = 'partials/config.html'; - -class GenericQueryOptionsCtrl {} -GenericQueryOptionsCtrl.templateUrl = 'partials/query.options.html'; - -export { - GenericDatasource as Datasource, - GenericDatasourceQueryCtrl as QueryCtrl, - GenericConfigCtrl as ConfigCtrl, - GenericQueryOptionsCtrl as QueryOptionsCtrl -}; diff --git a/examples/datasource-plugin-genericdatasource/src/partials/config.html b/examples/datasource-plugin-genericdatasource/src/partials/config.html deleted file mode 100644 index 30470039554..00000000000 --- a/examples/datasource-plugin-genericdatasource/src/partials/config.html +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/examples/datasource-plugin-genericdatasource/src/partials/query.editor.html b/examples/datasource-plugin-genericdatasource/src/partials/query.editor.html deleted file mode 100644 index b24cb0cb884..00000000000 --- a/examples/datasource-plugin-genericdatasource/src/partials/query.editor.html +++ /dev/null @@ -1,8 +0,0 @@ - -
    -
  • Query
  • -
  • - -
  • -
-
diff --git a/examples/datasource-plugin-genericdatasource/src/partials/query.options.html b/examples/datasource-plugin-genericdatasource/src/partials/query.options.html deleted file mode 100644 index b4600118683..00000000000 --- a/examples/datasource-plugin-genericdatasource/src/partials/query.options.html +++ /dev/null @@ -1,4 +0,0 @@ -
-
-
-
diff --git a/examples/datasource-plugin-genericdatasource/src/query_ctrl.js b/examples/datasource-plugin-genericdatasource/src/query_ctrl.js deleted file mode 100644 index 30c572b6892..00000000000 --- a/examples/datasource-plugin-genericdatasource/src/query_ctrl.js +++ /dev/null @@ -1,26 +0,0 @@ -import {QueryCtrl} from 'app/plugins/sdk'; -import './css/query-editor.css!' - -export class GenericDatasourceQueryCtrl extends QueryCtrl { - - constructor($scope, $injector, uiSegmentSrv) { - super($scope, $injector); - - this.scope = $scope; - this.uiSegmentSrv = uiSegmentSrv; - this.target.target = this.target.target || 'select metric'; - } - - getOptions() { - return this.datasource.metricFindQuery(this.target) - .then(this.uiSegmentSrv.transformToSegments(false)); - // Options have to be transformed by uiSegmentSrv to be usable by metric-segment-model directive - } - - onChangeInternal() { - this.panelCtrl.refresh(); // Asks the panel to refresh data. - } -} - -GenericDatasourceQueryCtrl.templateUrl = 'partials/query.editor.html'; - diff --git a/examples/nginx-app/.gitignore b/examples/nginx-app/.gitignore deleted file mode 100644 index 8c2c350441b..00000000000 --- a/examples/nginx-app/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -.DS_Store - -node_modules -tmp/* -npm-debug.log -dist/* - diff --git a/examples/nginx-app/.jscs.json b/examples/nginx-app/.jscs.json deleted file mode 100644 index dcf694dcc63..00000000000 --- a/examples/nginx-app/.jscs.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "disallowImplicitTypeConversion": ["string"], - "disallowKeywords": ["with"], - "disallowMultipleLineBreaks": true, - "disallowMixedSpacesAndTabs": true, - "disallowTrailingWhitespace": true, - "requireSpacesInFunctionExpression": { - "beforeOpeningCurlyBrace": true - }, - "disallowSpacesInsideArrayBrackets": true, - "disallowSpacesInsideParentheses": true, - "validateIndentation": 2 -} \ No newline at end of file diff --git a/examples/nginx-app/.jshintrc b/examples/nginx-app/.jshintrc deleted file mode 100644 index 3725af83afc..00000000000 --- a/examples/nginx-app/.jshintrc +++ /dev/null @@ -1,36 +0,0 @@ -{ - "browser": true, - "esnext": true, - - "bitwise":false, - "curly": true, - "eqnull": true, - "devel": true, - "eqeqeq": true, - "forin": false, - "immed": true, - "supernew": true, - "expr": true, - "indent": 2, - "latedef": true, - "newcap": true, - "noarg": true, - "noempty": true, - "undef": true, - "boss": true, - "trailing": true, - "laxbreak": true, - "laxcomma": true, - "sub": true, - "unused": true, - "maxdepth": 6, - "maxlen": 140, - - "globals": { - "System": true, - "define": true, - "require": true, - "Chromath": false, - "setImmediate": true - } -} diff --git a/examples/nginx-app/Gruntfile.js b/examples/nginx-app/Gruntfile.js deleted file mode 100644 index d36a4716f31..00000000000 --- a/examples/nginx-app/Gruntfile.js +++ /dev/null @@ -1,54 +0,0 @@ -module.exports = function(grunt) { - - require('load-grunt-tasks')(grunt); - - grunt.loadNpmTasks('grunt-execute'); - grunt.loadNpmTasks('grunt-contrib-clean'); - - grunt.initConfig({ - - clean: ["dist"], - - copy: { - src_to_dist: { - cwd: 'src', - expand: true, - src: ['**/*', '!**/*.js', '!**/*.scss'], - dest: 'dist' - }, - pluginDef: { - expand: true, - src: 'plugin.json', - dest: 'dist', - } - }, - - watch: { - rebuild_all: { - files: ['src/**/*', 'plugin.json'], - tasks: ['default'], - options: {spawn: false} - }, - }, - - babel: { - options: { - sourceMap: true, - presets: ["es2015"], - plugins: ['transform-es2015-modules-systemjs', "transform-es2015-for-of"], - }, - dist: { - files: [{ - cwd: 'src', - expand: true, - src: ['**/*.js'], - dest: 'dist', - ext:'.js' - }] - }, - }, - - }); - - grunt.registerTask('default', ['clean', 'copy:src_to_dist', 'copy:pluginDef', 'babel']); -}; diff --git a/examples/nginx-app/package.json b/examples/nginx-app/package.json deleted file mode 100644 index 91c53734ec8..00000000000 --- a/examples/nginx-app/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "kentik-app", - "private": true, - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/raintank/kentik-app-poc.git" - }, - "author": "", - "license": "ISC", - "bugs": { - "url": "https://github.com/raintank/kentik-app-poc/issues" - }, - "devDependencies": { - "grunt": "~0.4.5", - "babel": "~6.5.1", - "grunt-babel": "~6.0.0", - "grunt-contrib-copy": "~0.8.2", - "grunt-contrib-watch": "^0.6.1", - "grunt-contrib-uglify": "~0.11.0", - "grunt-systemjs-builder": "^0.2.5", - "load-grunt-tasks": "~3.2.0", - "grunt-execute": "~0.2.2", - "grunt-contrib-clean": "~0.6.0" - }, - "dependencies": { - "babel-plugin-transform-es2015-modules-systemjs": "^6.5.0", - "babel-preset-es2015": "^6.5.0", - "lodash": "~4.0.0" - }, - "homepage": "https://github.com/raintank/kentik-app-poc#readme" -} diff --git a/examples/nginx-app/src/components/config.html b/examples/nginx-app/src/components/config.html deleted file mode 100644 index c531ec36d76..00000000000 --- a/examples/nginx-app/src/components/config.html +++ /dev/null @@ -1,3 +0,0 @@ -

- Nginx config! -

diff --git a/examples/nginx-app/src/components/config.js b/examples/nginx-app/src/components/config.js deleted file mode 100644 index bb8f007b9bc..00000000000 --- a/examples/nginx-app/src/components/config.js +++ /dev/null @@ -1,6 +0,0 @@ - -export class NginxAppConfigCtrl { -} -NginxAppConfigCtrl.templateUrl = 'components/config.html'; - - diff --git a/examples/nginx-app/src/components/logs.html b/examples/nginx-app/src/components/logs.html deleted file mode 100644 index ca215772bf5..00000000000 --- a/examples/nginx-app/src/components/logs.html +++ /dev/null @@ -1,3 +0,0 @@ -

- Logs page! -

diff --git a/examples/nginx-app/src/components/logs.js b/examples/nginx-app/src/components/logs.js deleted file mode 100644 index 5b67290381b..00000000000 --- a/examples/nginx-app/src/components/logs.js +++ /dev/null @@ -1,6 +0,0 @@ - -export class LogsPageCtrl { -} -LogsPageCtrl.templateUrl = 'components/logs.html'; - - diff --git a/examples/nginx-app/src/components/stream.html b/examples/nginx-app/src/components/stream.html deleted file mode 100644 index ad70ca4df50..00000000000 --- a/examples/nginx-app/src/components/stream.html +++ /dev/null @@ -1,3 +0,0 @@ -

- Stream page! -

diff --git a/examples/nginx-app/src/components/stream.js b/examples/nginx-app/src/components/stream.js deleted file mode 100644 index 8684b36c64d..00000000000 --- a/examples/nginx-app/src/components/stream.js +++ /dev/null @@ -1,6 +0,0 @@ - -export class StreamPageCtrl { -} -StreamPageCtrl.templateUrl = 'components/stream.html'; - - diff --git a/examples/nginx-app/src/css/dark.css b/examples/nginx-app/src/css/dark.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/examples/nginx-app/src/css/light.css b/examples/nginx-app/src/css/light.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/examples/nginx-app/src/dashboards/dashboard.js b/examples/nginx-app/src/dashboards/dashboard.js deleted file mode 100644 index 794e2c5217b..00000000000 --- a/examples/nginx-app/src/dashboards/dashboard.js +++ /dev/null @@ -1,17 +0,0 @@ -require([ -], function () { - - function Dashboard() { - - this.getInputs = function() { - - }; - - this.buildDashboard = function() { - - }; - } - - return Dashboard; -}); - diff --git a/examples/nginx-app/src/img/logo_large.png b/examples/nginx-app/src/img/logo_large.png deleted file mode 100644 index c28955960e4..00000000000 Binary files a/examples/nginx-app/src/img/logo_large.png and /dev/null differ diff --git a/examples/nginx-app/src/img/logo_small.png b/examples/nginx-app/src/img/logo_small.png deleted file mode 100644 index a6040f66f3d..00000000000 Binary files a/examples/nginx-app/src/img/logo_small.png and /dev/null differ diff --git a/examples/nginx-app/src/module.js b/examples/nginx-app/src/module.js deleted file mode 100644 index b5aeecc6ccf..00000000000 --- a/examples/nginx-app/src/module.js +++ /dev/null @@ -1,9 +0,0 @@ -import {LogsPageCtrl} from './components/logs'; -import {StreamPageCtrl} from './components/stream'; -import {NginxAppConfigCtrl} from './components/config'; - -export { - NginxAppConfigCtrl as ConfigCtrl, - StreamPageCtrl, - LogsPageCtrl -}; diff --git a/examples/nginx-app/src/panel/module.js b/examples/nginx-app/src/panel/module.js deleted file mode 100644 index 899586da81b..00000000000 --- a/examples/nginx-app/src/panel/module.js +++ /dev/null @@ -1,15 +0,0 @@ -import {PanelCtrl} from 'app/plugins/sdk'; - -class NginxPanelCtrl extends PanelCtrl { - - constructor($scope, $injector) { - super($scope, $injector); - } - -} -NginxPanelCtrl.template = '

nginx!

'; - -export { - NginxPanelCtrl as PanelCtrl -}; - diff --git a/examples/nginx-app/src/panel/plugin.json b/examples/nginx-app/src/panel/plugin.json deleted file mode 100644 index f3548c987f3..00000000000 --- a/examples/nginx-app/src/panel/plugin.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "panel", - "name": "Nginx Panel", - "id": "nginx-panel" -} diff --git a/examples/panel-boilerplate-es5/module.js b/examples/panel-boilerplate-es5/module.js deleted file mode 100644 index b991ad72f76..00000000000 --- a/examples/panel-boilerplate-es5/module.js +++ /dev/null @@ -1,28 +0,0 @@ -define([ - 'app/plugins/sdk' -], function(sdk) { - - var BoilerPlatePanel = (function(_super) { - - function BoilerPlatePanel($scope, $injector) { - _super.call(this, $scope, $injector); - } - - // you do not need a templateUrl, you can use a inline template here - // BoilerPlatePanel.template = '

boilerplate

'; - - // all panel static assets can be accessed via 'public/plugins// - BoilerPlatePanel.templateUrl = 'panel.html'; - - BoilerPlatePanel.prototype = Object.create(_super.prototype); - BoilerPlatePanel.prototype.constructor = BoilerPlatePanel; - - return BoilerPlatePanel; - - })(sdk.PanelCtrl); - - - return { - PanelCtrl: BoilerPlatePanel - }; -}); diff --git a/examples/panel-boilerplate-es5/panel.html b/examples/panel-boilerplate-es5/panel.html deleted file mode 100644 index 5d3e44c756a..00000000000 --- a/examples/panel-boilerplate-es5/panel.html +++ /dev/null @@ -1,4 +0,0 @@ -

- Boilerplate panel -

- diff --git a/package.json b/package.json index a0b883fb463..1c8af93eb5f 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "devDependencies": { "angular2": "2.0.0-beta.0", + "autoprefixer": "^6.3.3", "es6-promise": "^3.0.2", "es6-shim": "^0.33.3", "expect.js": "~0.2.0", @@ -33,6 +34,7 @@ "grunt-karma": "~0.12.1", "grunt-ng-annotate": "^1.0.1", "grunt-notify": "^0.4.3", + "grunt-postcss": "^0.8.0", "grunt-sass": "^1.1.0", "grunt-string-replace": "~1.2.1", "grunt-systemjs-builder": "^0.2.5", @@ -72,6 +74,7 @@ "grunt-sync": "^0.4.1", "karma-sinon": "^1.0.3", "lodash": "^2.4.1", + "remarkable": "^1.6.2", "sinon": "1.16.1", "systemjs-builder": "^0.15.7", "tether": "^1.2.0", diff --git a/pkg/api/api.go b/pkg/api/api.go index 7242db9713a..b6cd4cd72bb 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -128,10 +128,6 @@ func Register(r *macaron.Macaron) { r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite)) r.Patch("/invites/:code/revoke", wrap(RevokeInvite)) - // apps - r.Get("/plugins", wrap(GetPluginList)) - r.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById)) - r.Post("/plugins/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting)) }, reqOrgAdmin) // create new org @@ -173,7 +169,18 @@ func Register(r *macaron.Macaron) { r.Put("/:id", bind(m.UpdateDataSourceCommand{}), UpdateDataSource) r.Delete("/:id", DeleteDataSource) r.Get("/:id", wrap(GetDataSourceById)) - r.Get("/plugins", GetDataSourcePlugins) + r.Get("/name/:name", wrap(GetDataSourceByName)) + }, reqOrgAdmin) + + r.Get("/datasources/id/:name", wrap(GetDataSourceIdByName), reqSignedIn) + + r.Group("/plugins", func() { + r.Get("/", wrap(GetPluginList)) + + 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) r.Get("/frontend/settings/", GetFrontendSettings) @@ -187,6 +194,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 a4e97cdb347..f5e5274a202 100644 --- a/pkg/api/cloudwatch/metrics.go +++ b/pkg/api/cloudwatch/metrics.go @@ -55,8 +55,10 @@ func init() { "S3BytesWritten", "S3BytesRead", "HDFSUtilization", "HDFSBytesRead", "HDFSBytesWritten", "MissingBlocks", "CorruptBlocks", "TotalLoad", "MemoryTotalMB", "MemoryReservedMB", "MemoryAvailableMB", "MemoryAllocatedMB", "PendingDeletionBlocks", "UnderReplicatedBlocks", "DfsPendingReplicationBlocks", "CapacityRemainingGB", "HbaseBackupFailed", "MostRecentBackupDuration", "TimeSinceLastSuccessfulBackup"}, "AWS/ES": {"ClusterStatus.green", "ClusterStatus.yellow", "ClusterStatus.red", "Nodes", "SearchableDocuments", "DeletedDocuments", "CPUUtilization", "FreeStorageSpace", "JVMMemoryPressure", "AutomatedSnapshotFailure", "MasterCPUUtilization", "MasterFreeStorageSpace", "MasterJVMMemoryPressure", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "DiskQueueLength", "ReadIOPS", "WriteIOPS"}, + "AWS/Events": {"Invocations", "FailedInvocations", "TriggeredRules", "MatchedEvents", "ThrottledRules"}, "AWS/Kinesis": {"PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "IncomingBytes", "IncomingRecords", "GetRecords.Bytes", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Success"}, "AWS/Lambda": {"Invocations", "Errors", "Duration", "Throttles"}, + "AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"}, "AWS/ML": {"PredictCount", "PredictFailureCount"}, "AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"}, "AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"}, @@ -85,8 +87,10 @@ func init() { "AWS/ELB": {"LoadBalancerName", "AvailabilityZone"}, "AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"}, "AWS/ES": {}, + "AWS/Events": {"RuleName"}, "AWS/Kinesis": {"StreamName"}, "AWS/Lambda": {"FunctionName"}, + "AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"}, "AWS/ML": {"MLModelId", "RequestMode"}, "AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"}, "AWS/Redshift": {"NodeID", "ClusterIdentifier"}, @@ -126,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/datasources.go b/pkg/api/datasources.go index 54959840d03..0982d73e88e 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -6,7 +6,6 @@ import ( //"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/util" ) @@ -52,24 +51,9 @@ func GetDataSourceById(c *middleware.Context) Response { } ds := query.Result + dtos := convertModelToDtos(ds) - return Json(200, &dtos.DataSource{ - Id: ds.Id, - OrgId: ds.OrgId, - Name: ds.Name, - Url: ds.Url, - Type: ds.Type, - Access: ds.Access, - Password: ds.Password, - Database: ds.Database, - User: ds.User, - BasicAuth: ds.BasicAuth, - BasicAuthUser: ds.BasicAuthUser, - BasicAuthPassword: ds.BasicAuthPassword, - WithCredentials: ds.WithCredentials, - IsDefault: ds.IsDefault, - JsonData: ds.JsonData, - }) + return Json(200, &dtos) } func DeleteDataSource(c *middleware.Context) { @@ -115,20 +99,58 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) { c.JsonOK("Datasource updated") } -func GetDataSourcePlugins(c *middleware.Context) { - dsList := make(map[string]*plugins.DataSourcePlugin) +// Get /api/datasources/name/:name +func GetDataSourceByName(c *middleware.Context) Response { + query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId} - if enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId); err != nil { - c.JsonApiErr(500, "Failed to get org apps", err) - return - } else { - - for key, value := range enabledPlugins.DataSources { - if !value.BuiltIn { - dsList[key] = value - } + if err := bus.Dispatch(&query); err != nil { + if err == m.ErrDataSourceNotFound { + return ApiError(404, "Data source not found", nil) } + return ApiError(500, "Failed to query datasources", err) + } - c.JSON(200, dsList) + ds := query.Result + dtos := convertModelToDtos(ds) + + return Json(200, &dtos) +} + +// Get /api/datasources/id/:name +func GetDataSourceIdByName(c *middleware.Context) Response { + query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId} + + if err := bus.Dispatch(&query); err != nil { + if err == m.ErrDataSourceNotFound { + return ApiError(404, "Data source not found", nil) + } + return ApiError(500, "Failed to query datasources", err) + } + + ds := query.Result + dtos := dtos.AnyId{ + Id: ds.Id, + } + + return Json(200, &dtos) +} + +func convertModelToDtos(ds m.DataSource) dtos.DataSource { + return dtos.DataSource{ + Id: ds.Id, + OrgId: ds.OrgId, + Name: ds.Name, + Url: ds.Url, + Type: ds.Type, + Access: ds.Access, + Password: ds.Password, + Database: ds.Database, + User: ds.User, + BasicAuth: ds.BasicAuth, + BasicAuthUser: ds.BasicAuthUser, + BasicAuthPassword: ds.BasicAuthPassword, + WithCredentials: ds.WithCredentials, + IsDefault: ds.IsDefault, + JsonData: ds.JsonData, } } diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index b810701233e..26295dd3d3c 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -6,10 +6,15 @@ import ( "strings" "time" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) +type AnyId struct { + Id int64 `json:"id"` +} + type LoginCommand struct { User string `json:"user" binding:"Required"` Password string `json:"password" binding:"Required"` @@ -48,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 af96202222f..7008c939aef 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -3,24 +3,32 @@ package dtos import "github.com/grafana/grafana/pkg/plugins" type PluginSetting struct { - Name string `json:"name"` - Type string `json:"type"` - PluginId string `json:"pluginId"` - Enabled bool `json:"enabled"` - Pinned bool `json:"pinned"` - Module string `json:"module"` - BaseUrl string `json:"baseUrl"` - Info *plugins.PluginInfo `json:"info"` - Pages []*plugins.AppPluginPage `json:"pages"` - Includes []*plugins.AppIncludeInfo `json:"includes"` - JsonData map[string]interface{} `json:"jsonData"` + Name string `json:"name"` + Type string `json:"type"` + Id string `json:"id"` + Enabled bool `json:"enabled"` + Pinned bool `json:"pinned"` + Module string `json:"module"` + BaseUrl string `json:"baseUrl"` + Info *plugins.PluginInfo `json:"info"` + Pages []*plugins.AppPluginPage `json:"pages"` + Includes []*plugins.PluginInclude `json:"includes"` + Dependencies *plugins.PluginDependencies `json:"dependencies"` + JsonData map[string]interface{} `json:"jsonData"` } type PluginListItem struct { - Name string `json:"name"` - Type string `json:"type"` - PluginId string `json:"pluginId"` - Enabled bool `json:"enabled"` - Pinned bool `json:"pinned"` - Info *plugins.PluginInfo `json:"info"` + Name string `json:"name"` + Type string `json:"type"` + Id string `json:"id"` + Enabled bool `json:"enabled"` + Pinned bool `json:"pinned"` + Info *plugins.PluginInfo `json:"info"` +} + +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/index.go b/pkg/api/index.go index 7cd2842b050..691c50f04f4 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -48,18 +48,23 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { data.User.LightTheme = true } + dashboardChildNavs := []*dtos.NavLink{ + {Text: "Home", Url: setting.AppSubUrl + "/"}, + {Text: "Playlists", Url: setting.AppSubUrl + "/playlists"}, + {Text: "Snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots"}, + } + + if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR { + dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Divider: true}) + dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Url: setting.AppSubUrl + "/dashboard/new"}) + dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Url: setting.AppSubUrl + "/import/dashboard"}) + } + data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ - Text: "Dashboards", - Icon: "icon-gf icon-gf-dashboard", - Url: setting.AppSubUrl + "/", - Children: []*dtos.NavLink{ - {Text: "Home", Url: setting.AppSubUrl + "/"}, - {Text: "Playlists", Url: setting.AppSubUrl + "/playlists"}, - {Text: "Snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots"}, - {Divider: true}, - {Text: "New", Url: setting.AppSubUrl + "/dashboard/new"}, - {Text: "Import", Url: setting.AppSubUrl + "/import/dashboard"}, - }, + Text: "Dashboards", + Icon: "icon-gf icon-gf-dashboard", + Url: setting.AppSubUrl + "/", + Children: dashboardChildNavs, }) if c.OrgRole == m.ROLE_ADMIN { diff --git a/pkg/api/login.go b/pkg/api/login.go index d0aace4235c..463fa8282a5 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -126,8 +126,10 @@ func loginUserWithUser(user *m.User, c *middleware.Context) { } days := 86400 * setting.LogInRememberDays - c.SetCookie(setting.CookieUserName, user.Login, days, setting.AppSubUrl+"/") - c.SetSuperSecureCookie(util.EncodeMd5(user.Rands+user.Password), setting.CookieRememberName, user.Login, days, setting.AppSubUrl+"/") + if days > 0 { + c.SetCookie(setting.CookieUserName, user.Login, days, setting.AppSubUrl+"/") + c.SetSuperSecureCookie(util.EncodeMd5(user.Rands+user.Password), setting.CookieRememberName, user.Login, days, setting.AppSubUrl+"/") + } c.Session.Set(middleware.SESS_KEY_USERID, user.Id) } diff --git a/pkg/api/plugin_setting.go b/pkg/api/plugins.go similarity index 51% rename from pkg/api/plugin_setting.go rename to pkg/api/plugins.go index 08fc5296434..195dd9483b8 100644 --- a/pkg/api/plugin_setting.go +++ b/pkg/api/plugins.go @@ -9,6 +9,10 @@ import ( ) func GetPluginList(c *middleware.Context) Response { + typeFilter := c.Query("type") + enabledFilter := c.Query("enabled") + embeddedFilter := c.Query("embedded") + pluginSettingsMap, err := plugins.GetPluginSettings(c.OrgId) if err != nil { @@ -17,11 +21,21 @@ func GetPluginList(c *middleware.Context) Response { result := make([]*dtos.PluginListItem, 0) for _, pluginDef := range plugins.Plugins { + // filter out app sub plugins + if embeddedFilter == "0" && pluginDef.IncludedInAppId != "" { + continue + } + + // filter on type + if typeFilter != "" && typeFilter != pluginDef.Type { + continue + } + listItem := &dtos.PluginListItem{ - PluginId: pluginDef.Id, - Name: pluginDef.Name, - Type: pluginDef.Type, - Info: &pluginDef.Info, + Id: pluginDef.Id, + Name: pluginDef.Name, + Type: pluginDef.Type, + Info: &pluginDef.Info, } if pluginSetting, exists := pluginSettingsMap[pluginDef.Id]; exists { @@ -29,6 +43,11 @@ func GetPluginList(c *middleware.Context) Response { listItem.Pinned = pluginSetting.Pinned } + // filter out disabled + if enabledFilter == "1" && !listItem.Enabled { + continue + } + result = append(result, listItem) } @@ -41,18 +60,20 @@ func GetPluginSettingById(c *middleware.Context) Response { if def, exists := plugins.Plugins[pluginId]; !exists { return ApiError(404, "Plugin not found, no installed plugin with that id", nil) } else { + dto := &dtos.PluginSetting{ - Type: def.Type, - PluginId: def.Id, - Name: def.Name, - Info: &def.Info, + Type: def.Type, + Id: def.Id, + Name: def.Name, + Info: &def.Info, + Dependencies: &def.Dependencies, + Includes: def.Includes, + BaseUrl: def.BaseUrl, + Module: def.Module, } if app, exists := plugins.Apps[pluginId]; exists { dto.Pages = app.Pages - dto.Includes = app.Includes - dto.BaseUrl = app.BaseUrl - dto.Module = app.Module } query := m.GetPluginSettingByIdQuery{PluginId: pluginId, OrgId: c.OrgId} @@ -86,3 +107,48 @@ func UpdatePluginSetting(c *middleware.Context, cmd m.UpdatePluginSettingCmd) Re return ApiSuccess("Plugin settings updated") } + +func GetPluginDashboards(c *middleware.Context) Response { + pluginId := c.Params(":pluginId") + + if list, err := plugins.GetPluginDashboards(c.OrgId, pluginId); err != nil { + if notfound, ok := err.(plugins.PluginNotFoundError); ok { + return ApiError(404, notfound.Error(), nil) + } + + return ApiError(500, "Failed to get plugin dashboards", err) + } else { + return Json(200, list) + } +} + +func GetPluginReadme(c *middleware.Context) Response { + pluginId := c.Params(":pluginId") + + 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, + Path: apiCmd.Path, + Inputs: apiCmd.Inputs, + } + + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to install dashboard", err) + } + + return Json(200, cmd.Result) +} diff --git a/pkg/api/render.go b/pkg/api/render.go index 728128acaab..9ed0c5ee6d7 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -31,6 +31,7 @@ func RenderToPng(c *middleware.Context) { Width: queryReader.Get("width", "800"), Height: queryReader.Get("height", "400"), SessionId: c.Session.ID(), + Timeout: queryReader.Get("timeout", "15"), } renderOpts.Url = setting.ToAbsUrl(renderOpts.Url) diff --git a/pkg/api/search.go b/pkg/api/search.go index 8f190dd6bf1..6123db5778c 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/live" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/services/search" + "strconv" ) func Search(c *middleware.Context) { @@ -17,13 +18,22 @@ func Search(c *middleware.Context) { limit = 1000 } + dbids := make([]int, 0) + for _, id := range c.QueryStrings("dashboardIds") { + dashboardId, err := strconv.Atoi(id) + if err == nil { + dbids = append(dbids, dashboardId) + } + } + searchQuery := search.Query{ - Title: query, - Tags: tags, - UserId: c.UserId, - Limit: limit, - IsStarred: starred == "true", - OrgId: c.OrgId, + Title: query, + Tags: tags, + UserId: c.UserId, + Limit: limit, + IsStarred: starred == "true", + OrgId: c.OrgId, + DashboardIds: dbids, } err := bus.Dispatch(&searchQuery) diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index 67e9f945cfd..f1b36c90ef2 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -11,7 +11,8 @@ func runCommand(command func(commandLine CommandLine) error) func(context *cli.C cmd := &contextCommandLine{context} if err := command(cmd); err != nil { - log.Errorf("%v\n\n", err) + log.Error("\nError: ") + log.Errorf("%s\n\n", err) cmd.ShowHelp() os.Exit(1) diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index 1d085cad89e..cf10444a0fe 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -4,6 +4,7 @@ import ( "archive/zip" "bytes" "errors" + "fmt" "github.com/grafana/grafana/pkg/cmd/grafana-cli/log" m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" @@ -13,6 +14,7 @@ import ( "os" "path" "regexp" + "strings" ) func validateInput(c CommandLine, pluginFolder string) error { @@ -26,8 +28,16 @@ func validateInput(c CommandLine, pluginFolder string) error { return errors.New("missing path flag") } - fileinfo, err := os.Stat(pluginDir) - if err != nil && !fileinfo.IsDir() { + fileInfo, err := os.Stat(pluginDir) + if err != nil { + if err = os.MkdirAll(pluginDir, os.ModePerm); err != nil { + return errors.New("path is not a directory") + } + + return nil + } + + if !fileInfo.IsDir() { return errors.New("path is not a directory") } @@ -43,13 +53,18 @@ func installCommand(c CommandLine) error { pluginToInstall := c.Args().First() version := c.Args().Get(1) - log.Infof("version: %v\n", version) + if version == "" { + log.Infof("version: latest\n") + } else { + log.Infof("version: %v\n", version) + } - return InstallPlugin(pluginToInstall, pluginFolder, version) + return InstallPlugin(pluginToInstall, version, c) } -func InstallPlugin(pluginName, pluginFolder, version string) error { - plugin, err := s.GetPlugin(pluginName) +func InstallPlugin(pluginName, version string, c CommandLine) error { + plugin, err := s.GetPlugin(pluginName, c.GlobalString("repo")) + pluginFolder := c.GlobalString("path") if err != nil { return err } @@ -59,28 +74,33 @@ func InstallPlugin(pluginName, pluginFolder, version string) error { return err } - url := v.Url - commit := v.Commit + if version == "" { + version = v.Version + } - downloadURL := url + "/archive/" + commit + ".zip" + downloadURL := fmt.Sprintf("%s/%s/versions/%s/download", + c.GlobalString("repo"), + pluginName, + version) log.Infof("installing %v @ %v\n", plugin.Id, version) log.Infof("from url: %v\n", downloadURL) - log.Infof("on commit: %v\n", commit) log.Infof("into: %v\n", pluginFolder) err = downloadFile(plugin.Id, pluginFolder, downloadURL) - if err == nil { - log.Infof("Installed %v successfully ✔\n", plugin.Id) + if err != nil { + return err } + log.Infof("Installed %v successfully ✔\n", plugin.Id) + + /* Enable once we need support for downloading depedencies res, _ := s.ReadPlugin(pluginFolder, pluginName) - for _, v := range res.Dependency.Plugins { - InstallPlugin(v.Id, pluginFolder, "") - log.Infof("Installed Dependency: %v ✔\n", v.Id) + InstallPlugin(v.Id, version, c) + log.Infof("Installed dependency: %v ✔\n", v.Id) } - + */ return err } @@ -98,12 +118,26 @@ func SelectVersion(plugin m.Plugin, version string) (m.Version, error) { return m.Version{}, errors.New("Could not find the version your looking for") } -func RemoveGitBuildFromname(pluginname, filename string) string { +func RemoveGitBuildFromName(pluginName, filename string) string { r := regexp.MustCompile("^[a-zA-Z0-9_.-]*/") - return r.ReplaceAllString(filename, pluginname+"/") + return r.ReplaceAllString(filename, pluginName+"/") } -func downloadFile(pluginName, filepath, url string) (err error) { +var retryCount = 0 + +func downloadFile(pluginName, filePath, url string) (err error) { + defer func() { + if r := recover(); r != nil { + retryCount++ + if retryCount == 1 { + log.Debug("\nFailed downloading. Will retry once.\n") + downloadFile(pluginName, filePath, url) + } else { + panic(r) + } + } + }() + resp, err := http.Get(url) if err != nil { return err @@ -120,14 +154,18 @@ func downloadFile(pluginName, filepath, url string) (err error) { return err } for _, zf := range r.File { - newfile := path.Join(filepath, RemoveGitBuildFromname(pluginName, zf.Name)) + newFile := path.Join(filePath, RemoveGitBuildFromName(pluginName, zf.Name)) if zf.FileInfo().IsDir() { - os.Mkdir(newfile, 0777) + os.Mkdir(newFile, 0777) } else { - dst, err := os.Create(newfile) + 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/cmd/grafana-cli/commands/install_command_test.go b/pkg/cmd/grafana-cli/commands/install_command_test.go index b88677cf614..52b329adf7f 100644 --- a/pkg/cmd/grafana-cli/commands/install_command_test.go +++ b/pkg/cmd/grafana-cli/commands/install_command_test.go @@ -19,7 +19,7 @@ func TestFoldernameReplacement(t *testing.T) { Convey("should be replaced with plugin name", func() { for k, v := range paths { - So(RemoveGitBuildFromname(pluginName, k), ShouldEqual, v) + So(RemoveGitBuildFromName(pluginName, k), ShouldEqual, v) } }) }) @@ -32,7 +32,7 @@ func TestFoldernameReplacement(t *testing.T) { Convey("should be replaced with plugin name", func() { for k, v := range paths { - So(RemoveGitBuildFromname(pluginName, k), ShouldEqual, v) + So(RemoveGitBuildFromName(pluginName, k), ShouldEqual, v) } }) }) diff --git a/pkg/cmd/grafana-cli/commands/listremote_command.go b/pkg/cmd/grafana-cli/commands/listremote_command.go index 7cb3d650c55..0f0c3077ab9 100644 --- a/pkg/cmd/grafana-cli/commands/listremote_command.go +++ b/pkg/cmd/grafana-cli/commands/listremote_command.go @@ -6,7 +6,7 @@ import ( ) func listremoteCommand(c CommandLine) error { - plugin, err := s.ListAllPlugins() + plugin, err := s.ListAllPlugins(c.GlobalString("repo")) if err != nil { return err diff --git a/pkg/cmd/grafana-cli/commands/ls_command.go b/pkg/cmd/grafana-cli/commands/ls_command.go index f863bc4b72b..05dd57bfd2a 100644 --- a/pkg/cmd/grafana-cli/commands/ls_command.go +++ b/pkg/cmd/grafana-cli/commands/ls_command.go @@ -2,6 +2,7 @@ package commands import ( "errors" + "fmt" "github.com/grafana/grafana/pkg/cmd/grafana-cli/log" m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" @@ -9,16 +10,16 @@ import ( var ls_getPlugins func(path string) []m.InstalledPlugin = s.GetLocalPlugins -var validateLsCommmand = func(pluginDir string) error { +var validateLsCommand = func(pluginDir string) error { if pluginDir == "" { return errors.New("missing path flag") } - log.Info("plugindir: " + pluginDir + "\n") + log.Debug("plugindir: " + pluginDir + "\n") pluginDirInfo, err := s.IoHelper.Stat(pluginDir) if err != nil { - return errors.New("missing path flag") + return fmt.Errorf("error: %s", err) } if pluginDirInfo.IsDir() == false { @@ -30,12 +31,18 @@ var validateLsCommmand = func(pluginDir string) error { func lsCommand(c CommandLine) error { pluginDir := c.GlobalString("path") - if err := validateLsCommmand(pluginDir); err != nil { + if err := validateLsCommand(pluginDir); err != nil { return err } - for _, plugin := range ls_getPlugins(pluginDir) { - log.Infof("plugin: %s @ %s \n", plugin.Name, plugin.Info.Version) + plugins := ls_getPlugins(pluginDir) + + if len(plugins) > 0 { + log.Info("installed plugins:\n") + } + + for _, plugin := range plugins { + log.Infof("%s @ %s \n", plugin.Id, plugin.Info.Version) } return nil diff --git a/pkg/cmd/grafana-cli/commands/ls_command_test.go b/pkg/cmd/grafana-cli/commands/ls_command_test.go index fa49375234d..650c6270bf0 100644 --- a/pkg/cmd/grafana-cli/commands/ls_command_test.go +++ b/pkg/cmd/grafana-cli/commands/ls_command_test.go @@ -9,10 +9,10 @@ import ( ) func TestMissingPath(t *testing.T) { - var org = validateLsCommmand + var org = validateLsCommand Convey("ls command", t, func() { - validateLsCommmand = org + validateLsCommand = org Convey("Missing path", func() { commandLine := &commandstest.FakeCommandLine{ @@ -61,7 +61,7 @@ func TestMissingPath(t *testing.T) { }, } - validateLsCommmand = func(pluginDir string) error { + validateLsCommand = func(pluginDir string) error { return errors.New("dummie error") } diff --git a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go b/pkg/cmd/grafana-cli/commands/upgrade_all_command.go index 6c1e0182f51..d8594182a99 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_all_command.go @@ -32,7 +32,7 @@ func upgradeAllCommand(c CommandLine) error { localPlugins := s.GetLocalPlugins(pluginDir) - remotePlugins, err := s.ListAllPlugins() + remotePlugins, err := s.ListAllPlugins(c.GlobalString("repo")) if err != nil { return err @@ -54,7 +54,7 @@ func upgradeAllCommand(c CommandLine) error { log.Infof("Upgrading %v \n", p.Id) s.RemoveInstalledPlugin(pluginDir, p.Id) - InstallPlugin(p.Id, pluginDir, "") + InstallPlugin(p.Id, "", c) } return nil diff --git a/pkg/cmd/grafana-cli/commands/upgrade_command.go b/pkg/cmd/grafana-cli/commands/upgrade_command.go index 5a4fe477c9b..e4072e5ced9 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_command.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_command.go @@ -14,7 +14,7 @@ func upgradeCommand(c CommandLine) error { return err } - remotePlugins, err2 := s.ListAllPlugins() + remotePlugins, err2 := s.ListAllPlugins(c.GlobalString("repo")) if err2 != nil { return err2 @@ -24,7 +24,7 @@ func upgradeCommand(c CommandLine) error { if localPlugin.Id == v.Id { if ShouldUpgrade(localPlugin.Info.Version, v) { s.RemoveInstalledPlugin(pluginDir, pluginName) - return InstallPlugin(localPlugin.Id, pluginDir, "") + return InstallPlugin(localPlugin.Id, "", c) } } } diff --git a/pkg/cmd/grafana-cli/main.go b/pkg/cmd/grafana-cli/main.go index 938cfcc284a..b277714fe9b 100644 --- a/pkg/cmd/grafana-cli/main.go +++ b/pkg/cmd/grafana-cli/main.go @@ -12,7 +12,9 @@ import ( var version = "master" func getGrafanaPluginPath() string { - //TODO: try to get path from os:env GF_PLUGIN_FOLDER + if os.Getenv("GF_PLUGIN_DIR") != "" { + return os.Getenv("GF_PLUGIN_DIR") + } os := runtime.GOOS if os == "windows" { @@ -36,6 +38,11 @@ func main() { Usage: "path to the grafana installation", Value: getGrafanaPluginPath(), }, + cli.StringFlag{ + Name: "repo", + Usage: "url to the plugin repository", + Value: "https://grafana-net.raintank.io/api/plugins", + }, cli.BoolFlag{ Name: "debug, d", Usage: "enable debug logging", diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go index d5051a623a3..cd03f755075 100644 --- a/pkg/cmd/grafana-cli/services/services.go +++ b/pkg/cmd/grafana-cli/services/services.go @@ -3,6 +3,7 @@ package services import ( "encoding/json" "errors" + "fmt" "github.com/franela/goreq" "github.com/grafana/grafana/pkg/cmd/grafana-cli/log" m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" @@ -11,8 +12,13 @@ import ( var IoHelper m.IoUtil = IoUtilImp{} -func ListAllPlugins() (m.PluginRepo, error) { - res, _ := goreq.Request{Uri: "https://raw.githubusercontent.com/grafana/grafana-plugin-repository/master/repo.json"}.Do() +func ListAllPlugins(repoUrl string) (m.PluginRepo, error) { + fullUrl := repoUrl + "/repo" + res, _ := goreq.Request{Uri: fullUrl, MaxRedirects: 3}.Do() + + if res.StatusCode != 200 { + return m.PluginRepo{}, fmt.Errorf("Could not access %s statuscode %v", fullUrl, res.StatusCode) + } var resp m.PluginRepo err := res.Body.FromJsonTo(&resp) @@ -59,16 +65,16 @@ func RemoveInstalledPlugin(pluginPath, id string) error { return IoHelper.RemoveAll(path.Join(pluginPath, id)) } -func GetPlugin(id string) (m.Plugin, error) { - resp, err := ListAllPlugins() +func GetPlugin(pluginId, repoUrl string) (m.Plugin, error) { + resp, err := ListAllPlugins(repoUrl) if err != nil { } for _, i := range resp.Plugins { - if i.Id == id { + if i.Id == pluginId { return i, nil } } - return m.Plugin{}, errors.New("could not find plugin named \"" + id + "\"") + return m.Plugin{}, errors.New("could not find plugin named \"" + pluginId + "\"") } diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index d78b8f700ff..dad3f437390 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -24,7 +24,7 @@ import ( "github.com/grafana/grafana/pkg/social" ) -var version = "master" +var version = "3.0.0-pre1" var commit = "NA" var buildstamp string var build_date string 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/renderer/renderer.go b/pkg/components/renderer/renderer.go index d72ceca9c3d..f81da43c295 100644 --- a/pkg/components/renderer/renderer.go +++ b/pkg/components/renderer/renderer.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" + "strconv" ) type RenderOpts struct { @@ -18,6 +19,7 @@ type RenderOpts struct { Width string Height string SessionId string + Timeout string } func RenderToPng(params *RenderOpts) (string, error) { @@ -60,8 +62,13 @@ func RenderToPng(params *RenderOpts) (string, error) { close(done) }() + timeout, err := strconv.Atoi(params.Timeout) + if err != nil { + timeout = 15 + } + select { - case <-time.After(15 * time.Second): + case <-time.After(time.Duration(timeout) * time.Second): if err := cmd.Process.Kill(); err != nil { log.Error(4, "failed to kill: %v", err) } 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 1015dfbe28c..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() @@ -102,13 +95,13 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { } // GetString a -func (dash *Dashboard) GetString(prop string) string { - return dash.Data[prop].(string) +func (dash *Dashboard) GetString(prop string, defaultValue string) string { + 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) } @@ -117,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/models/plugin_setting.go b/pkg/models/plugin_settings.go similarity index 91% rename from pkg/models/plugin_setting.go rename to pkg/models/plugin_settings.go index 777f4599241..d030c125ba9 100644 --- a/pkg/models/plugin_setting.go +++ b/pkg/models/plugin_settings.go @@ -61,7 +61,14 @@ func (cmd *UpdatePluginSettingCmd) GetEncryptedJsonData() SecureJsonData { // QUERIES type GetPluginSettingsQuery struct { OrgId int64 - Result []*PluginSetting + Result []*PluginSettingInfoDTO +} + +type PluginSettingInfoDTO struct { + OrgId int64 + PluginId string + Enabled bool + Pinned bool } type GetPluginSettingByIdQuery struct { diff --git a/pkg/plugins/app_plugin.go b/pkg/plugins/app_plugin.go index ac7ef8948b0..7fc170784f3 100644 --- a/pkg/plugins/app_plugin.go +++ b/pkg/plugins/app_plugin.go @@ -21,20 +21,13 @@ type AppPluginCss struct { Dark string `json:"dark"` } -type AppIncludeInfo struct { - Name string `json:"name"` - Type string `json:"type"` - Id string `json:"id"` -} - type AppPlugin struct { FrontendPluginBase - Pages []*AppPluginPage `json:"pages"` - Routes []*AppPluginRoute `json:"routes"` - Includes []*AppIncludeInfo `json:"-"` + Pages []*AppPluginPage `json:"pages"` + Routes []*AppPluginRoute `json:"routes"` - Pinned bool `json:"-"` - Enabled bool `json:"-"` + FoundChildPlugins []*PluginInclude `json:"-"` + Pinned bool `json:"-"` } type AppPluginRoute struct { @@ -71,7 +64,7 @@ func (app *AppPlugin) initApp() { for _, panel := range Panels { if strings.HasPrefix(panel.PluginDir, app.PluginDir) { panel.setPathsBasedOnApp(app) - app.Includes = append(app.Includes, &AppIncludeInfo{ + app.FoundChildPlugins = append(app.FoundChildPlugins, &PluginInclude{ Name: panel.Name, Id: panel.Id, Type: panel.Type, @@ -83,7 +76,7 @@ func (app *AppPlugin) initApp() { for _, ds := range DataSources { if strings.HasPrefix(ds.PluginDir, app.PluginDir) { ds.setPathsBasedOnApp(app) - app.Includes = append(app.Includes, &AppIncludeInfo{ + app.FoundChildPlugins = append(app.FoundChildPlugins, &PluginInclude{ Name: ds.Name, Id: ds.Id, Type: ds.Type, 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/dashboards.go b/pkg/plugins/dashboards.go new file mode 100644 index 00000000000..932196a42a9 --- /dev/null +++ b/pkg/plugins/dashboards.go @@ -0,0 +1,91 @@ +package plugins + +import ( + "os" + "path/filepath" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" +) + +type PluginDashboardInfoDTO struct { + PluginId string `json:"pluginId"` + Title string `json:"title"` + Installed bool `json:"installed"` + InstalledUri string `json:"installedUri"` + InstalledRevision string `json:"installedRevision"` + Revision string `json:"revision"` + Description string `json:"description"` + Path string `json:"path"` +} + +func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDTO, error) { + plugin, exists := Plugins[pluginId] + + if !exists { + return nil, PluginNotFoundError{pluginId} + } + + result := make([]*PluginDashboardInfoDTO, 0) + + for _, include := range plugin.Includes { + if include.Type == PluginTypeDashboard { + if dashInfo, err := getDashboardImportStatus(orgId, plugin, include.Path); err != nil { + return nil, err + } else { + result = append(result, dashInfo) + } + } + } + + return result, nil +} + +func loadPluginDashboard(plugin *PluginBase, path string) (*m.Dashboard, error) { + + dashboardFilePath := filepath.Join(plugin.PluginDir, path) + reader, err := os.Open(dashboardFilePath) + if err != nil { + return nil, err + } + + defer reader.Close() + + data, err := simplejson.NewFromReader(reader) + if err != nil { + return nil, err + } + + return m.NewDashboardFromJson(data), nil +} + +func getDashboardImportStatus(orgId int64, plugin *PluginBase, path string) (*PluginDashboardInfoDTO, error) { + res := &PluginDashboardInfoDTO{} + + var dashboard *m.Dashboard + var err error + + if dashboard, err = loadPluginDashboard(plugin, path); err != nil { + return nil, err + } + + res.Path = path + res.PluginId = plugin.Id + res.Title = dashboard.Title + res.Revision = dashboard.GetString("revision", "1.0") + + query := m.GetDashboardQuery{OrgId: orgId, Slug: dashboard.Slug} + + if err := bus.Dispatch(&query); err != nil { + if err != m.ErrDashboardNotFound { + return nil, err + } + } else { + res.Installed = true + res.InstalledUri = "db/" + query.Result.Slug + res.InstalledRevision = query.Result.GetString("revision", "1.0") + } + + return res, nil +} diff --git a/pkg/plugins/dashboards_test.go b/pkg/plugins/dashboards_test.go new file mode 100644 index 00000000000..bdd08ceefd2 --- /dev/null +++ b/pkg/plugins/dashboards_test.go @@ -0,0 +1,53 @@ +package plugins + +import ( + "testing" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/ini.v1" +) + +func TestPluginDashboards(t *testing.T) { + + Convey("When asking plugin dashboard info", t, func() { + setting.Cfg = ini.Empty() + sec, _ := setting.Cfg.NewSection("plugin.test-app") + sec.NewKey("path", "../../tests/test-app") + err := Init() + + So(err, ShouldBeNil) + + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { + if query.Slug == "nginx-connections" { + dash := m.NewDashboard("Nginx Connections") + dash.Data.Set("revision", "1.1") + query.Result = dash + return nil + } + + return m.ErrDashboardNotFound + }) + + dashboards, err := GetPluginDashboards(1, "test-app") + + So(err, ShouldBeNil) + + Convey("should return 2 dashboarrd", func() { + So(len(dashboards), ShouldEqual, 2) + }) + + Convey("should include installed version info", func() { + So(dashboards[0].Title, ShouldEqual, "Nginx Connections") + So(dashboards[0].Revision, ShouldEqual, "1.5") + So(dashboards[0].InstalledRevision, ShouldEqual, "1.1") + So(dashboards[0].InstalledUri, ShouldEqual, "db/nginx-connections") + + So(dashboards[1].Revision, ShouldEqual, "2.0") + So(dashboards[1].InstalledRevision, ShouldEqual, "") + }) + }) + +} diff --git a/pkg/plugins/datasource_plugin.go b/pkg/plugins/datasource_plugin.go index ecffb87b8db..b8c79f22998 100644 --- a/pkg/plugins/datasource_plugin.go +++ b/pkg/plugins/datasource_plugin.go @@ -4,12 +4,11 @@ import "encoding/json" type DataSourcePlugin struct { FrontendPluginBase - DefaultMatchFormat string `json:"defaultMatchFormat"` - Annotations bool `json:"annotations"` - Metrics bool `json:"metrics"` - BuiltIn bool `json:"builtIn"` - Mixed bool `json:"mixed"` - App string `json:"app"` + Annotations bool `json:"annotations"` + Metrics bool `json:"metrics"` + BuiltIn bool `json:"builtIn"` + Mixed bool `json:"mixed"` + App string `json:"app"` } func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error { diff --git a/pkg/plugins/frontend_plugin.go b/pkg/plugins/frontend_plugin.go index 5acb9966495..aab3f851f60 100644 --- a/pkg/plugins/frontend_plugin.go +++ b/pkg/plugins/frontend_plugin.go @@ -11,10 +11,6 @@ import ( type FrontendPluginBase struct { PluginBase - Module string `json:"module"` - BaseUrl string `json:"baseUrl"` - StaticRoot string `json:"staticRoot"` - StaticRootAbs string `json:"-"` } func (fp *FrontendPluginBase) initFrontendPlugin() { @@ -28,11 +24,11 @@ func (fp *FrontendPluginBase) initFrontendPlugin() { fp.handleModuleDefaults() - fp.Info.Logos.Small = evalRelativePluginUrlPath(fp.Info.Logos.Small, fp.Id) - fp.Info.Logos.Large = evalRelativePluginUrlPath(fp.Info.Logos.Large, fp.Id) + fp.Info.Logos.Small = evalRelativePluginUrlPath(fp.Info.Logos.Small, fp.BaseUrl) + fp.Info.Logos.Large = evalRelativePluginUrlPath(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.Id) + fp.Info.Screenshots[i].Path = evalRelativePluginUrlPath(fp.Info.Screenshots[i].Path, fp.BaseUrl) } } @@ -55,7 +51,7 @@ func (fp *FrontendPluginBase) handleModuleDefaults() { fp.BaseUrl = path.Join("public/app/plugins", fp.Type, fp.Id) } -func evalRelativePluginUrlPath(pathStr string, pluginId string) string { +func evalRelativePluginUrlPath(pathStr string, baseUrl string) string { if pathStr == "" { return "" } @@ -64,5 +60,5 @@ func evalRelativePluginUrlPath(pathStr string, pluginId string) string { if u.IsAbs() { return pathStr } - return path.Join("public/plugins", pluginId, pathStr) + return path.Join(baseUrl, pathStr) } diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 83f43c1f544..07c3d321ec7 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -3,24 +3,49 @@ package plugins import ( "encoding/json" "errors" + "fmt" "strings" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/setting" ) +var ( + PluginTypeApp = "app" + PluginTypeDatasource = "datasource" + PluginTypePanel = "panel" + PluginTypeDashboard = "dashboard" +) + +type PluginNotFoundError struct { + PluginId string +} + +func (e PluginNotFoundError) Error() string { + return fmt.Sprintf("Plugin with id %s not found", e.PluginId) +} + type PluginLoader interface { Load(decoder *json.Decoder, pluginDir string) error } type PluginBase struct { - Type string `json:"type"` - Name string `json:"name"` - Id string `json:"id"` - Info PluginInfo `json:"info"` + 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:"-"` IncludedInAppId string `json:"-"` PluginDir string `json:"-"` + + // cache for readme file contents + Readme []byte `json:"-"` } func (pb *PluginBase) registerPlugin(pluginDir string) error { @@ -32,11 +57,38 @@ func (pb *PluginBase) registerPlugin(pluginDir string) error { log.Info("Plugins: Registering plugin %v", pb.Name) } + if len(pb.Dependencies.Plugins) == 0 { + pb.Dependencies.Plugins = []PluginDependencyItem{} + } + + if pb.Dependencies.GrafanaVersion == "" { + pb.Dependencies.GrafanaVersion = "*" + } + pb.PluginDir = pluginDir Plugins[pb.Id] = pb return nil } +type PluginDependencies struct { + GrafanaVersion string `json:"grafanaVersion"` + Plugins []PluginDependencyItem `json:"plugins"` +} + +type PluginInclude struct { + Name string `json:"name"` + Path string `json:"path"` + Type string `json:"type"` + Id string `json:"id"` +} + +type PluginDependencyItem struct { + Type string `json:"type"` + Id string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` +} + type PluginInfo struct { Author PluginInfoLink `json:"author"` Description string `json:"description"` diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index dcf8a9f0630..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" @@ -89,12 +90,6 @@ func scan(pluginDir string) error { pluginPath: pluginDir, } - log.Info("Plugins: Scaning dir %s", pluginDir) - if util.ContainsDistFolder(pluginDir) { - log.Info("Plugins: Found dist folder in %s", pluginDir) - pluginDir = filepath.Join(pluginDir, "dist") - } - if err := util.Walk(pluginDir, true, true, scanner.walker); err != nil { if pluginDir != "data/plugins" { log.Warn("Could not scan dir \"%v\" error: %s", pluginDir, err) @@ -161,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/plugins/plugins_test.go b/pkg/plugins/plugins_test.go index 08ff3cbdfdd..f2dbc1e2e82 100644 --- a/pkg/plugins/plugins_test.go +++ b/pkg/plugins/plugins_test.go @@ -27,14 +27,15 @@ func TestPluginScans(t *testing.T) { Convey("When reading app plugin definition", t, func() { setting.Cfg = ini.Empty() - sec, _ := setting.Cfg.NewSection("plugin.app-test") - sec.NewKey("path", "../../tests/app-plugin-json") + sec, _ := setting.Cfg.NewSection("plugin.nginx-app") + sec.NewKey("path", "../../tests/test-app") err := Init() So(err, ShouldBeNil) So(len(Apps), ShouldBeGreaterThan, 0) - So(Apps["app-example"].Info.Logos.Large, ShouldEqual, "public/plugins/app-example/img/logo_large.png") - So(Apps["app-example"].Info.Screenshots[1].Path, ShouldEqual, "public/plugins/app-example/img/screenshot2.png") + + So(Apps["test-app"].Info.Logos.Large, ShouldEqual, "public/plugins/test-app/img/logo_large.png") + So(Apps["test-app"].Info.Screenshots[1].Path, ShouldEqual, "public/plugins/test-app/img/screenshot2.png") }) } diff --git a/pkg/plugins/queries.go b/pkg/plugins/queries.go index b3cc92bf7d1..b930c9575a3 100644 --- a/pkg/plugins/queries.go +++ b/pkg/plugins/queries.go @@ -5,61 +5,71 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -func GetPluginSettings(orgId int64) (map[string]*m.PluginSetting, error) { +func GetPluginSettings(orgId int64) (map[string]*m.PluginSettingInfoDTO, error) { query := m.GetPluginSettingsQuery{OrgId: orgId} if err := bus.Dispatch(&query); err != nil { return nil, err } - pluginMap := make(map[string]*m.PluginSetting) + pluginMap := make(map[string]*m.PluginSettingInfoDTO) for _, plug := range query.Result { pluginMap[plug.PluginId] = plug } + for _, pluginDef := range Plugins { + // ignore entries that exists + if _, ok := pluginMap[pluginDef.Id]; ok { + continue + } + + // default to enabled true + opt := &m.PluginSettingInfoDTO{Enabled: true} + + // if it's included in app check app settings + if pluginDef.IncludedInAppId != "" { + // app componets are by default disabled + opt.Enabled = false + + if appSettings, ok := pluginMap[pluginDef.IncludedInAppId]; ok { + opt.Enabled = appSettings.Enabled + } + } + + pluginMap[pluginDef.Id] = opt + } + return pluginMap, nil } func GetEnabledPlugins(orgId int64) (*EnabledPlugins, error) { enabledPlugins := NewEnabledPlugins() - orgPlugins, err := GetPluginSettings(orgId) + pluginSettingMap, err := GetPluginSettings(orgId) if err != nil { return nil, err } - enabledApps := make(map[string]bool) + isPluginEnabled := func(pluginId string) bool { + _, ok := pluginSettingMap[pluginId] + return ok + } for pluginId, app := range Apps { - - if b, ok := orgPlugins[pluginId]; ok { - app.Enabled = b.Enabled + if b, ok := pluginSettingMap[pluginId]; ok { app.Pinned = b.Pinned - } - - if app.Enabled { - enabledApps[pluginId] = true enabledPlugins.Apps = append(enabledPlugins.Apps, app) } } - isPluginEnabled := func(appId string) bool { - if appId == "" { - return true - } - - _, ok := enabledApps[appId] - return ok - } - // add all plugins that are not part of an App. for dsId, ds := range DataSources { - if isPluginEnabled(ds.IncludedInAppId) { + if isPluginEnabled(ds.Id) { enabledPlugins.DataSources[dsId] = ds } } for _, panel := range Panels { - if isPluginEnabled(panel.IncludedInAppId) { + if isPluginEnabled(panel.Id) { enabledPlugins.Panels = append(enabledPlugins.Panels, panel) } } diff --git a/pkg/services/search/handlers.go b/pkg/services/search/handlers.go index 1c480992cbc..a4905d6fa58 100644 --- a/pkg/services/search/handlers.go +++ b/pkg/services/search/handlers.go @@ -39,10 +39,11 @@ func searchHandler(query *Query) error { hits := make(HitList, 0) dashQuery := FindPersistedDashboardsQuery{ - Title: query.Title, - UserId: query.UserId, - IsStarred: query.IsStarred, - OrgId: query.OrgId, + Title: query.Title, + UserId: query.UserId, + IsStarred: query.IsStarred, + OrgId: query.OrgId, + DashboardIds: query.DashboardIds, } if err := bus.Dispatch(&dashQuery); err != 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/search/models.go b/pkg/services/search/models.go index 9b8c7627f89..159637013f5 100644 --- a/pkg/services/search/models.go +++ b/pkg/services/search/models.go @@ -25,21 +25,23 @@ func (s HitList) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s HitList) Less(i, j int) bool { return s[i].Title < s[j].Title } type Query struct { - Title string - Tags []string - OrgId int64 - UserId int64 - Limit int - IsStarred bool + Title string + Tags []string + OrgId int64 + UserId int64 + Limit int + IsStarred bool + DashboardIds []int Result HitList } type FindPersistedDashboardsQuery struct { - Title string - OrgId int64 - UserId int64 - IsStarred bool + Title string + OrgId int64 + UserId int64 + IsStarred bool + DashboardIds []int Result HitList } diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 2a8ff2dc941..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 @@ -146,6 +146,19 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error { params = append(params, query.UserId) } + if len(query.DashboardIds) > 0 { + sql.WriteString(" AND (") + for i, dashboardId := range query.DashboardIds { + if i != 0 { + sql.WriteString(" OR") + } + + sql.WriteString(" dashboard.id = ?") + params = append(params, dashboardId) + } + sql.WriteString(")") + } + if len(query.Title) > 0 { sql.WriteString(" AND dashboard.title " + dialect.LikeStr() + " ?") params = append(params, "%"+query.Title+"%") @@ -154,6 +167,7 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error { sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000")) var res []DashboardSearchProjection + err := x.Sql(sql.String(), params...).Find(&res) if err != nil { return err 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 0d4eb111868..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) @@ -32,6 +33,8 @@ func TestDashboardDataAccess(t *testing.T) { Convey("Given saved dashboard", func() { savedDash := insertTestDashboard("test dash 23", 1, "prod", "webapp") + insertTestDashboard("test dash 45", 1, "prod") + insertTestDashboard("test dash 67", 1, "prod", "webapp") Convey("Should return dashboard model", func() { So(savedDash.Title, ShouldEqual, "test dash 23") @@ -56,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) @@ -74,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) @@ -87,7 +90,7 @@ func TestDashboardDataAccess(t *testing.T) { Convey("Should be able to search for dashboard", func() { query := search.FindPersistedDashboardsQuery{ - Title: "test", + Title: "test dash 23", OrgId: 1, } @@ -99,14 +102,45 @@ func TestDashboardDataAccess(t *testing.T) { So(len(hit.Tags), ShouldEqual, 2) }) + Convey("Should be able to search for dashboard by dashboard ids", func() { + Convey("should be able to find two dashboards by id", func() { + query := search.FindPersistedDashboardsQuery{ + DashboardIds: []int{1, 2}, + OrgId: 1, + } + + err := SearchDashboards(&query) + So(err, ShouldBeNil) + + So(len(query.Result), ShouldEqual, 2) + + hit := query.Result[0] + So(len(hit.Tags), ShouldEqual, 2) + + hit2 := query.Result[1] + So(len(hit2.Tags), ShouldEqual, 1) + }) + + Convey("DashboardIds that does not exists should not cause errors", func() { + query := search.FindPersistedDashboardsQuery{ + DashboardIds: []int{1000}, + OrgId: 1, + } + + err := SearchDashboards(&query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 0) + }) + }) + 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/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 28ea3035bcd..7a6ba554246 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -21,6 +21,7 @@ func AddMigrations(mg *Migrator) { addAppSettingsMigration(mg) addSessionMigration(mg) addPlaylistMigrations(mg) + addPreferencesMigrations(mg) } func addMigrationLogMigrations(mg *Migrator) { diff --git a/pkg/services/sqlstore/migrations/preferences_mig.go b/pkg/services/sqlstore/migrations/preferences_mig.go new file mode 100644 index 00000000000..0ce01857b75 --- /dev/null +++ b/pkg/services/sqlstore/migrations/preferences_mig.go @@ -0,0 +1,20 @@ +package migrations + +import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +func addPreferencesMigrations(mg *Migrator) { + + preferencesV1 := Table{ + Name: "preferences", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "pref_id", Type: DB_Int, Nullable: false}, + {Name: "pref_type", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "pref_data", Type: DB_Text, Nullable: false}, + }, + } + + // create table + mg.AddMigration("create preferences table v1", NewAddTableMigration(preferencesV1)) + +} diff --git a/pkg/services/sqlstore/plugin_setting.go b/pkg/services/sqlstore/plugin_setting.go index 53f4857d9b6..b3285e905cf 100644 --- a/pkg/services/sqlstore/plugin_setting.go +++ b/pkg/services/sqlstore/plugin_setting.go @@ -16,9 +16,12 @@ func init() { } func GetPluginSettings(query *m.GetPluginSettingsQuery) error { - sess := x.Where("org_id=?", query.OrgId) + sql := `SELECT org_id, plugin_id, enabled, pinned + FROM plugin_setting + WHERE org_id=?` - query.Result = make([]*m.PluginSetting, 0) + sess := x.Sql(sql, query.OrgId) + query.Result = make([]*m.PluginSettingInfoDTO, 0) return sess.Find(&query.Result) } diff --git a/pkg/util/filepath.go b/pkg/util/filepath.go index c70f9260197..3ad8cac3147 100644 --- a/pkg/util/filepath.go +++ b/pkg/util/filepath.go @@ -80,35 +80,50 @@ func walk(path string, info os.FileInfo, resolvedPath string, symlinkPathsFollow if err != nil { return walkFn(resolvedPath, info, err) } + var subFiles = make([]subFile, 0) for _, fileInfo := range list { path2 := filepath.Join(path, fileInfo.Name()) var resolvedPath2 string if resolvedPath != "" { resolvedPath2 = filepath.Join(resolvedPath, fileInfo.Name()) } - err = walk(path2, fileInfo, resolvedPath2, symlinkPathsFollowed, walkFn) + subFiles = append(subFiles, subFile{path: path2, resolvedPath: resolvedPath2, fileInfo: fileInfo}) + } + + if containsDistFolder(subFiles) { + err := walk( + filepath.Join(path, "dist"), + info, + filepath.Join(resolvedPath, "dist"), + symlinkPathsFollowed, + walkFn) + if err != nil { return err } + } else { + for _, p := range subFiles { + err = walk(p.path, p.fileInfo, p.resolvedPath, symlinkPathsFollowed, walkFn) + + if err != nil { + return err + } + } } + return nil } return nil } -func ContainsDistFolder(path string) bool { - info, err := os.Lstat(path) - if err != nil { - return false - } +type subFile struct { + path, resolvedPath string + fileInfo os.FileInfo +} - if !info.IsDir() { - return false - } - - list, err := ioutil.ReadDir(path) - for _, fileInfo := range list { - if fileInfo.IsDir() && fileInfo.Name() == "dist" { +func containsDistFolder(subFiles []subFile) bool { + for _, p := range subFiles { + if p.fileInfo.IsDir() && p.fileInfo.Name() == "dist" { return true } } diff --git a/public/app/core/directives/plugin_component.ts b/public/app/core/directives/plugin_component.ts index 858aab02035..69535327cc8 100644 --- a/public/app/core/directives/plugin_component.ts +++ b/public/app/core/directives/plugin_component.ts @@ -148,12 +148,13 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ } // ConfigCtrl case 'datasource-config-ctrl': { - return System.import(scope.datasourceMeta.module).then(function(dsModule) { + var dsMeta = scope.ctrl.datasourceMeta; + return System.import(dsMeta.module).then(function(dsModule) { return { - baseUrl: scope.datasourceMeta.baseUrl, - name: 'ds-config-' + scope.datasourceMeta.id, + baseUrl: dsMeta.baseUrl, + name: 'ds-config-' + dsMeta.id, bindings: {meta: "=", current: "="}, - attrs: {meta: "datasourceMeta", current: "current"}, + attrs: {meta: "ctrl.datasourceMeta", current: "ctrl.current"}, Component: dsModule.ConfigCtrl, }; }); diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 6e3af683c82..92cf32448a1 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -49,20 +49,22 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controller : 'DashboardImportCtrl', }) .when('/datasources', { - templateUrl: 'public/app/features/datasources/partials/list.html', + templateUrl: 'public/app/features/plugins/partials/ds_list.html', controller : 'DataSourcesCtrl', controllerAs: 'ctrl', - resolve: loadOrgBundle, + resolve: loadPluginsBundle, }) .when('/datasources/edit/:id', { - templateUrl: 'public/app/features/datasources/partials/edit.html', + templateUrl: 'public/app/features/plugins/partials/ds_edit.html', controller : 'DataSourceEditCtrl', - resolve: loadOrgBundle, + controllerAs: 'ctrl', + resolve: loadPluginsBundle, }) .when('/datasources/new', { - templateUrl: 'public/app/features/datasources/partials/edit.html', + templateUrl: 'public/app/features/plugins/partials/ds_edit.html', controller : 'DataSourceEditCtrl', - resolve: loadOrgBundle, + controllerAs: 'ctrl', + resolve: loadPluginsBundle, }) .when('/org', { templateUrl: 'public/app/features/org/partials/orgDetails.html', @@ -166,19 +168,19 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controllerAs: 'ctrl', }) .when('/plugins', { - templateUrl: 'public/app/features/plugins/partials/list.html', + templateUrl: 'public/app/features/plugins/partials/plugin_list.html', controller: 'PluginListCtrl', controllerAs: 'ctrl', resolve: loadPluginsBundle, }) .when('/plugins/:pluginId/edit', { - templateUrl: 'public/app/features/plugins/partials/edit.html', + templateUrl: 'public/app/features/plugins/partials/plugin_edit.html', controller: 'PluginEditCtrl', controllerAs: 'ctrl', resolve: loadPluginsBundle, }) .when('/plugins/:pluginId/page/:slug', { - templateUrl: 'public/app/features/plugins/partials/page.html', + templateUrl: 'public/app/features/plugins/partials/plugin_page.html', controller: 'AppPageCtrl', controllerAs: 'ctrl', resolve: loadPluginsBundle, diff --git a/public/app/core/services/alert_srv.js b/public/app/core/services/alert_srv.js index d833a0667ac..70965d2947f 100644 --- a/public/app/core/services/alert_srv.js +++ b/public/app/core/services/alert_srv.js @@ -66,6 +66,7 @@ function (angular, _, coreModule) { scope.title = payload.title; scope.text = payload.text; + scope.text2 = payload.text2; scope.onConfirm = payload.onConfirm; scope.icon = payload.icon || "fa-check"; scope.yesText = payload.yesText || "Yes"; @@ -74,7 +75,7 @@ function (angular, _, coreModule) { var confirmModal = $modal({ template: 'public/app/partials/confirm_modal.html', persist: false, - modalClass: 'modal-no-header confirm-modal', + modalClass: 'confirm-modal', show: false, scope: scope, keyboard: false diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index e17d4c53f5a..f5fcf5bd50c 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -180,4 +180,21 @@ export default class TimeSeries { } return false; } + + hideFromLegend(options) { + if (options.hideEmpty && this.allIsNull) { + return true; + } + // ignore series excluded via override + if (!this.legend) { + return true; + } + + // ignore zero series + if (options.hideZero && this.allIsZero) { + return true; + } + + return false; + } } diff --git a/public/app/features/admin/adminListOrgsCtrl.js b/public/app/features/admin/adminListOrgsCtrl.js index c3f727191fc..d68e92955ee 100644 --- a/public/app/features/admin/adminListOrgsCtrl.js +++ b/public/app/features/admin/adminListOrgsCtrl.js @@ -20,8 +20,9 @@ function (angular) { $scope.deleteOrg = function(org) { $scope.appEvent('confirm-modal', { - title: 'Do you want to delete organization ' + org.name + '?', - text: 'All dashboards for this organization will be removed!', + title: 'Delete', + text: 'Do you want to delete organization ' + org.name + '?', + text2: 'All dashboards for this organization will be removed!', icon: 'fa-trash', yesText: 'Delete', onConfirm: function() { diff --git a/public/app/features/admin/adminListUsersCtrl.js b/public/app/features/admin/adminListUsersCtrl.js index c89deafaf0f..721adcfb98c 100644 --- a/public/app/features/admin/adminListUsersCtrl.js +++ b/public/app/features/admin/adminListUsersCtrl.js @@ -20,7 +20,8 @@ function (angular) { $scope.deleteUser = function(user) { $scope.appEvent('confirm-modal', { - title: 'Do you want to delete ' + user.login + '?', + title: 'Delete', + text: 'Do you want to delete ' + user.login + '?', icon: 'fa-trash', yesText: 'Delete', onConfirm: function() { diff --git a/public/app/features/dashboard/dashboardLoaderSrv.js b/public/app/features/dashboard/dashboardLoaderSrv.js index eba0a962629..1af0894b462 100644 --- a/public/app/features/dashboard/dashboardLoaderSrv.js +++ b/public/app/features/dashboard/dashboardLoaderSrv.js @@ -8,7 +8,7 @@ define([ './impression_store', 'app/core/config', ], -function (angular, moment, _, $, kbn, dateMath, impressionStore, config) { +function (angular, moment, _, $, kbn, dateMath, impressionStore) { 'use strict'; var module = angular.module('grafana.services'); @@ -48,12 +48,7 @@ function (angular, moment, _, $, kbn, dateMath, impressionStore, config) { promise.then(function(result) { if (result.meta.dashboardNotFound !== true) { - impressionStore.impressions.addDashboardImpression({ - type: type, - slug: slug, - title: result.dashboard.title, - orgId: config.bootData.user.orgId - }); + impressionStore.impressions.addDashboardImpression(result.dashboard.id); } return result; diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index 01c5787481b..fbd950e60b6 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -212,7 +212,7 @@ function (angular, $, _, moment) { var i, j, k; var oldVersion = this.schemaVersion; var panelUpgrades = []; - this.schemaVersion = 10; + this.schemaVersion = 11; if (oldVersion === this.schemaVersion) { return; @@ -401,6 +401,14 @@ function (angular, $, _, moment) { }); } + if (oldVersion < 11) { + // update template variables + _.each(this.templating.list, function(templateVariable) { + if (templateVariable.refresh) { templateVariable.refresh = 1; } + if (!templateVariable.refresh) { templateVariable.refresh = 0; } + }); + } + if (panelUpgrades.length === 0) { return; } diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index 6cddafc03b8..2d5b66cb13c 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -108,8 +108,9 @@ export class DashNavCtrl { err.isHandled = true; $scope.appEvent('confirm-modal', { - title: 'Someone else has updated this dashboard!', - text: "Would you still like to save this dashboard?", + title: 'Conflict', + text: 'Someone else has updated this dashboard.', + text2: 'Would you still like to save this dashboard?', yesText: "Save & Overwrite", icon: "fa-warning", onConfirm: function() { @@ -122,8 +123,9 @@ export class DashNavCtrl { err.isHandled = true; $scope.appEvent('confirm-modal', { - title: 'Another dashboard with the same name exists', - text: "Would you still like to save this dashboard?", + title: 'Conflict', + text: 'Dashboard with the same name exists.', + text2: 'Would you still like to save this dashboard?', yesText: "Save & Overwrite", icon: "fa-warning", onConfirm: function() { @@ -135,7 +137,9 @@ export class DashNavCtrl { $scope.deleteDashboard = function() { $scope.appEvent('confirm-modal', { - title: 'Do you want to delete dashboard ' + $scope.dashboard.title + '?', + title: 'Delete', + text: 'Do you want to delete this dashboard?', + text2: $scope.dashboard.title, icon: 'fa-trash', yesText: 'Delete', onConfirm: function() { diff --git a/public/app/features/dashboard/impression_store.ts b/public/app/features/dashboard/impression_store.ts index 7f7ecb9a9ba..4bd979a6b34 100644 --- a/public/app/features/dashboard/impression_store.ts +++ b/public/app/features/dashboard/impression_store.ts @@ -6,7 +6,7 @@ import _ from 'lodash'; export class ImpressionsStore { constructor() {} - addDashboardImpression(impression) { + addDashboardImpression(dashboardId) { var impressions = []; if (store.exists("dashboard_impressions")) { impressions = JSON.parse(store.get("dashboard_impressions")); @@ -16,25 +16,27 @@ export class ImpressionsStore { } impressions = impressions.filter((imp) => { - return impression.slug !== imp.slug; + return dashboardId !== imp; }); - impressions.unshift({ - title: impression.title, - slug: impression.slug, - orgId: impression.orgId, - type: impression.type - }); + impressions.unshift(dashboardId); - if (impressions.length > 20) { - impressions.shift(); + if (impressions.length > 50) { + impressions.pop(); } store.set("dashboard_impressions", JSON.stringify(impressions)); } getDashboardOpened() { - var impressions = store.get("dashboard_impressions"); - return JSON.parse(impressions || "[]"); + var impressions = store.get("dashboard_impressions") || "[]"; + + impressions = JSON.parse(impressions); + + impressions = _.filter(impressions, el => { + return _.isNumber(el); + }); + + return impressions; } } diff --git a/public/app/features/dashboard/partials/settings.html b/public/app/features/dashboard/partials/settings.html index 8fd2b34ae5a..deccd22305b 100644 --- a/public/app/features/dashboard/partials/settings.html +++ b/public/app/features/dashboard/partials/settings.html @@ -19,7 +19,6 @@
-
Dashboard Detail
diff --git a/public/app/features/dashboard/partials/shareModal.html b/public/app/features/dashboard/partials/shareModal.html index a6d439baadf..41c08fede0b 100644 --- a/public/app/features/dashboard/partials/shareModal.html +++ b/public/app/features/dashboard/partials/shareModal.html @@ -54,11 +54,11 @@
Include - +
Include - +
Theme diff --git a/public/app/features/dashboard/rowCtrl.js b/public/app/features/dashboard/rowCtrl.js index d7ccc22603f..ece5ebdfbf7 100644 --- a/public/app/features/dashboard/rowCtrl.js +++ b/public/app/features/dashboard/rowCtrl.js @@ -52,7 +52,8 @@ function (angular, _, config) { } $scope.appEvent('confirm-modal', { - title: 'Are you sure you want to delete this row?', + title: 'Delete', + text: 'Are you sure you want to delete this row?', icon: 'fa-trash', yesText: 'Delete', onConfirm: function() { diff --git a/public/app/features/dashboard/shareModalCtrl.js b/public/app/features/dashboard/shareModalCtrl.js index 0ba27ec6f5d..2944cc79e42 100644 --- a/public/app/features/dashboard/shareModalCtrl.js +++ b/public/app/features/dashboard/shareModalCtrl.js @@ -1,5 +1,4 @@ -define([ - 'angular', +define(['angular', 'lodash', 'require', 'app/core/config', @@ -72,6 +71,7 @@ function (angular, _, require, config) { var soloUrl = $scope.shareUrl; soloUrl = soloUrl.replace('/dashboard/', '/dashboard-solo/'); + soloUrl = soloUrl.replace("&fullscreen", ""); $scope.iframeHtml = ''; diff --git a/public/app/features/dashboard/unsavedChangesSrv.js b/public/app/features/dashboard/unsavedChangesSrv.js index 0b9ef4e18f4..ffc82e198cd 100644 --- a/public/app/features/dashboard/unsavedChangesSrv.js +++ b/public/app/features/dashboard/unsavedChangesSrv.js @@ -140,7 +140,7 @@ function(angular, _) { $rootScope.appEvent('show-modal', { src: 'public/app/partials/unsaved-changes.html', - modalClass: 'modal-no-header confirm-modal', + modalClass: 'confirm-modal', scope: modalScope, }); }; diff --git a/public/app/features/dashboard/viewStateSrv.js b/public/app/features/dashboard/viewStateSrv.js index b18610d7c1d..f3112d6db50 100644 --- a/public/app/features/dashboard/viewStateSrv.js +++ b/public/app/features/dashboard/viewStateSrv.js @@ -129,7 +129,6 @@ function (angular, _, $) { ctrl.editMode = false; ctrl.fullscreen = false; - delete ctrl.height; this.$scope.appEvent('panel-fullscreen-exit', {panelId: ctrl.panel.id}); @@ -147,13 +146,9 @@ function (angular, _, $) { }; DashboardViewState.prototype.enterFullscreen = function(panelScope) { - var docHeight = $(window).height(); - var editHeight = Math.floor(docHeight * 0.3); - var fullscreenHeight = Math.floor(docHeight * 0.7); var ctrl = panelScope.ctrl; ctrl.editMode = this.state.edit && this.$scope.dashboardMeta.canEdit; - ctrl.height = ctrl.editMode ? editHeight : fullscreenHeight; ctrl.fullscreen = true; this.oldTimeRange = ctrl.range; diff --git a/public/app/features/datasources/all.js b/public/app/features/datasources/all.js deleted file mode 100644 index b181fd475c2..00000000000 --- a/public/app/features/datasources/all.js +++ /dev/null @@ -1,4 +0,0 @@ -define([ - './list_ctrl', - './edit_ctrl', -], function () {}); diff --git a/public/app/features/datasources/edit_ctrl.js b/public/app/features/datasources/edit_ctrl.js deleted file mode 100644 index cbb3cfdd4c5..00000000000 --- a/public/app/features/datasources/edit_ctrl.js +++ /dev/null @@ -1,121 +0,0 @@ -define([ - 'angular', - 'lodash', - 'app/core/config', -], -function (angular, _, config) { - 'use strict'; - - var module = angular.module('grafana.controllers'); - var datasourceTypes = []; - - module.directive('datasourceHttpSettings', function() { - return { - scope: {current: "="}, - templateUrl: 'public/app/features/datasources/partials/http_settings.html' - }; - }); - - module.controller('DataSourceEditCtrl', function($scope, $q, backendSrv, $routeParams, $location, datasourceSrv) { - - var defaults = {name: '', type: 'graphite', url: '', access: 'proxy', jsonData: {}}; - - $scope.init = function() { - $scope.isNew = true; - $scope.datasources = []; - - $scope.loadDatasourceTypes().then(function() { - if ($routeParams.id) { - $scope.getDatasourceById($routeParams.id); - } else { - $scope.current = angular.copy(defaults); - $scope.typeChanged(); - } - }); - }; - - $scope.loadDatasourceTypes = function() { - if (datasourceTypes.length > 0) { - $scope.types = datasourceTypes; - return $q.when(null); - } - - return backendSrv.get('/api/datasources/plugins').then(function(plugins) { - datasourceTypes = plugins; - $scope.types = plugins; - }); - }; - - $scope.getDatasourceById = function(id) { - backendSrv.get('/api/datasources/' + id).then(function(ds) { - $scope.isNew = false; - $scope.current = ds; - return $scope.typeChanged(); - }); - }; - - $scope.typeChanged = function() { - $scope.datasourceMeta = $scope.types[$scope.current.type]; - }; - - $scope.updateFrontendSettings = function() { - return backendSrv.get('/api/frontend/settings').then(function(settings) { - config.datasources = settings.datasources; - config.defaultDatasource = settings.defaultDatasource; - datasourceSrv.init(); - }); - }; - - $scope.testDatasource = function() { - $scope.testing = { done: false }; - - datasourceSrv.get($scope.current.name).then(function(datasource) { - if (!datasource.testDatasource) { - $scope.testing.message = 'Data source does not support test connection feature.'; - $scope.testing.status = 'warning'; - $scope.testing.title = 'Unknown'; - return; - } - - return datasource.testDatasource().then(function(result) { - $scope.testing.message = result.message; - $scope.testing.status = result.status; - $scope.testing.title = result.title; - }, function(err) { - if (err.statusText) { - $scope.testing.message = err.statusText; - $scope.testing.title = "HTTP Error"; - } else { - $scope.testing.message = err.message; - $scope.testing.title = "Unknown error"; - } - }); - }).finally(function() { - $scope.testing.done = true; - }); - }; - - $scope.saveChanges = function(test) { - if (!$scope.editForm.$valid) { - return; - } - - if ($scope.current.id) { - return backendSrv.put('/api/datasources/' + $scope.current.id, $scope.current).then(function() { - $scope.updateFrontendSettings().then(function() { - if (test) { - $scope.testDatasource(); - } - }); - }); - } else { - return backendSrv.post('/api/datasources', $scope.current).then(function(result) { - $scope.updateFrontendSettings(); - $location.path('datasources/edit/' + result.id); - }); - } - }; - - $scope.init(); - }); -}); diff --git a/public/app/features/datasources/partials/edit.html b/public/app/features/datasources/partials/edit.html deleted file mode 100644 index 695043569fd..00000000000 --- a/public/app/features/datasources/partials/edit.html +++ /dev/null @@ -1,59 +0,0 @@ - - - -
- - -
-
-
- Name - - - The name is used when you select the data source in panels. - The Default data source is preselected in new - panels. - - - -
- -
- Type -
- -
-
- -
- - - - - - -
-
Testing....
-
Test results
-
-
{{testing.title}}
-
-
-
- -
- - - - Cancel -
-
-
diff --git a/public/app/features/org/all.js b/public/app/features/org/all.js index cebd0dd1def..e04634d709a 100644 --- a/public/app/features/org/all.js +++ b/public/app/features/org/all.js @@ -4,5 +4,4 @@ define([ './userInviteCtrl', './orgApiKeysCtrl', './orgDetailsCtrl', - '../datasources/all', ], function () {}); diff --git a/public/app/features/org/org_users_ctrl.ts b/public/app/features/org/org_users_ctrl.ts index 16c1cce268b..9e70328f345 100644 --- a/public/app/features/org/org_users_ctrl.ts +++ b/public/app/features/org/org_users_ctrl.ts @@ -38,7 +38,7 @@ export class OrgUsersCtrl { removeUser(user) { this.$scope.appEvent('confirm-modal', { - title: 'Confirm delete user', + title: 'Delete', text: 'Are you sure you want to delete user ' + user.login + '?', yesText: "Delete", icon: "fa-warning", diff --git a/public/app/features/org/partials/apikeyModal.html b/public/app/features/org/partials/apikeyModal.html index 2af81e57ac8..a4f59e1b86e 100644 --- a/public/app/features/org/partials/apikeyModal.html +++ b/public/app/features/org/partials/apikeyModal.html @@ -26,7 +26,7 @@ You can authenticate request using the Authorization HTTP header, example:

-
+			
 curl -H "Authorization: Bearer your_key_above" http://your.grafana.com/api/dashboards/db/mydash
 			
diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 87fdb2390fa..2385354fa91 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -3,6 +3,11 @@ import config from 'app/core/config'; import _ from 'lodash'; import angular from 'angular'; +import $ from 'jquery'; + +const TITLE_HEIGHT = 25; +const EMPTY_TITLE_HEIGHT = 9; +const PANEL_PADDING = 5; export class PanelCtrl { panel: any; @@ -20,6 +25,9 @@ export class PanelCtrl { inspector: any; editModeInitiated: boolean; editorHelpIndex: number; + editMode: any; + height: any; + containerHeight: any; constructor($scope, $injector) { this.$injector = $injector; @@ -34,10 +42,12 @@ export class PanelCtrl { } $scope.$on("refresh", () => this.refresh()); + $scope.$on("render", () => this.calculatePanelHeight()); } init() { this.publishAppEvent('panel-instantiated', {scope: this.$scope}); + this.calculatePanelHeight(); this.refresh(); } @@ -111,6 +121,23 @@ export class PanelCtrl { return this.dashboard.meta.fullscreen && !this.fullscreen; } + calculatePanelHeight() { + + if (this.fullscreen) { + var docHeight = $(window).height(); + var editHeight = Math.floor(docHeight * 0.3); + var fullscreenHeight = Math.floor(docHeight * 0.7); + this.containerHeight = this.editMode ? editHeight : fullscreenHeight; + } else { + this.containerHeight = this.panel.height || this.row.height; + if (_.isString(this.containerHeight)) { + this.containerHeight = parseInt(this.containerHeight.replace('px', ''), 10); + } + } + + this.height = this.containerHeight - (PANEL_PADDING + (this.panel.title ? TITLE_HEIGHT : EMPTY_TITLE_HEIGHT)); + } + broadcastRender(arg1?, arg2?) { this.$scope.$broadcast('render', arg1, arg2); } @@ -136,9 +163,10 @@ export class PanelCtrl { removePanel() { this.publishAppEvent('confirm-modal', { - title: 'Are you sure you want to remove this panel?', + title: 'Remove Panel', + text: 'Are you sure you want to remove this panel?', icon: 'fa-trash', - yesText: 'Delete', + yesText: 'Remove', onConfirm: () => { this.row.panels = _.without(this.row.panels, this.panel); } @@ -171,9 +199,9 @@ export class PanelCtrl { shareScope.dashboard = this.dashboard; this.publishAppEvent('show-modal', { - src: 'public/app/features/dashboard/partials/shareModal.html', - scope: shareScope - }); + src: 'public/app/features/dashboard/partials/shareModal.html', + scope: shareScope + }); } openInspector() { diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index a15224a622a..755e56de71a 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -65,8 +65,8 @@ module.directive('grafanaPanel', function() { link: function(scope, elem) { var panelContainer = elem.find('.panel-container'); var ctrl = scope.ctrl; - scope.$watchGroup(['ctrl.fullscreen', 'ctrl.height', 'ctrl.panel.height', 'ctrl.row.height'], function() { - panelContainer.css({ minHeight: ctrl.height || ctrl.panel.height || ctrl.row.height, display: 'block' }); + scope.$watchGroup(['ctrl.fullscreen', 'ctrl.containerHeight'], function() { + panelContainer.css({minHeight: ctrl.containerHeight}); elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false); }); } diff --git a/public/app/features/panel/panel_helper.js b/public/app/features/panel/panel_helper.js deleted file mode 100644 index 7287acd7b44..00000000000 --- a/public/app/features/panel/panel_helper.js +++ /dev/null @@ -1,135 +0,0 @@ -define([ - 'angular', - 'lodash', - 'jquery', - 'app/core/utils/kbn', - 'app/core/utils/datemath', - 'app/core/utils/rangeutil', -], -function (angular, _, $, kbn, dateMath, rangeUtil) { - 'use strict'; - - var module = angular.module('grafana.services'); - - module.service('panelHelper', function(timeSrv, $rootScope, $q) { - var self = this; - - this.setTimeQueryStart = function(scope) { - scope.timing = {}; - scope.timing.queryStart = new Date().getTime(); - }; - - this.setTimeQueryEnd = function(scope) { - scope.timing.queryEnd = new Date().getTime(); - }; - - this.setTimeRenderStart = function(scope) { - scope.timing = scope.timing || {}; - scope.timing.renderStart = new Date().getTime(); - }; - - this.setTimeRenderEnd = function(scope) { - scope.timing.renderEnd = new Date().getTime(); - }; - - this.broadcastRender = function(scope, arg1, arg2) { - this.setTimeRenderStart(scope); - scope.$broadcast('render', arg1, arg2); - this.setTimeRenderEnd(scope); - - if ($rootScope.profilingEnabled) { - $rootScope.performance.panels.push({ - panelId: scope.panel.id, - query: scope.timing.queryEnd - scope.timing.queryStart, - render: scope.timing.renderEnd - scope.timing.renderStart, - }); - } - }; - - this.updateTimeRange = function(scope) { - scope.range = timeSrv.timeRange(); - scope.rangeRaw = timeSrv.timeRange(false); - - this.applyPanelTimeOverrides(scope); - - if (scope.panel.maxDataPoints) { - scope.resolution = scope.panel.maxDataPoints; - } - else { - scope.resolution = Math.ceil($(window).width() * (scope.panel.span / 12)); - } - - var panelInterval = scope.panel.interval; - var datasourceInterval = (scope.datasource || {}).interval; - scope.interval = kbn.calculateInterval(scope.range, scope.resolution, panelInterval || datasourceInterval); - }; - - this.applyPanelTimeOverrides = function(scope) { - scope.panelMeta.timeInfo = ''; - - // check panel time overrrides - if (scope.panel.timeFrom) { - var timeFromInfo = rangeUtil.describeTextRange(scope.panel.timeFrom); - if (timeFromInfo.invalid) { - scope.panelMeta.timeFromInfo = 'invalid time override'; - return; - } - - if (_.isString(scope.rangeRaw.from)) { - var timeFromDate = dateMath.parse(timeFromInfo.from); - scope.panelMeta.timeInfo = timeFromInfo.display; - scope.rangeRaw.from = timeFromInfo.from; - scope.rangeRaw.to = timeFromInfo.to; - scope.range.from = timeFromDate; - } - } - - if (scope.panel.timeShift) { - var timeShiftInfo = rangeUtil.describeTextRange(scope.panel.timeShift); - if (timeShiftInfo.invalid) { - scope.panelMeta.timeInfo = 'invalid timeshift'; - return; - } - - var timeShift = '-' + scope.panel.timeShift; - scope.panelMeta.timeInfo += ' timeshift ' + timeShift; - scope.range.from = dateMath.parseDateMath(timeShift, scope.range.from, false); - scope.range.to = dateMath.parseDateMath(timeShift, scope.range.to, true); - - scope.rangeRaw = scope.range; - } - - if (scope.panel.hideTimeOverride) { - scope.panelMeta.timeInfo = ''; - } - }; - - this.issueMetricQuery = function(scope, datasource) { - if (!scope.panel.targets || scope.panel.targets.length === 0) { - return $q.when([]); - } - - var metricsQuery = { - range: scope.range, - rangeRaw: scope.rangeRaw, - interval: scope.interval, - targets: scope.panel.targets, - format: scope.panel.renderer === 'png' ? 'png' : 'json', - maxDataPoints: scope.resolution, - scopedVars: scope.panel.scopedVars, - cacheTimeout: scope.panel.cacheTimeout - }; - - this.setTimeQueryStart(scope); - return datasource.query(metricsQuery).then(function(results) { - self.setTimeQueryEnd(scope); - - if (scope.dashboard.snapshot) { - scope.panel.snapshotData = results; - } - - return results; - }); - }; - }); -}); diff --git a/public/app/features/playlist/partials/playlist.html b/public/app/features/playlist/partials/playlist.html index c06bd5b8e0e..08c31522b7f 100644 --- a/public/app/features/playlist/partials/playlist.html +++ b/public/app/features/playlist/partials/playlist.html @@ -62,7 +62,7 @@ - diff --git a/public/app/features/playlist/playlists_ctrl.ts b/public/app/features/playlist/playlists_ctrl.ts index 10d6d86d309..59758f0db1f 100644 --- a/public/app/features/playlist/playlists_ctrl.ts +++ b/public/app/features/playlist/playlists_ctrl.ts @@ -30,10 +30,10 @@ export class PlaylistsCtrl { removePlaylist(playlist) { this.$scope.appEvent('confirm-modal', { - title: 'Confirm delete playlist', + title: 'Delete', text: 'Are you sure you want to delete playlist ' + playlist.name + '?', yesText: "Delete", - icon: "fa-warning", + icon: "fa-trash", onConfirm: () => { this.removePlaylistConfirmed(playlist); } diff --git a/public/app/features/plugins/all.ts b/public/app/features/plugins/all.ts index 9d54165e56b..346fb2b30ef 100644 --- a/public/app/features/plugins/all.ts +++ b/public/app/features/plugins/all.ts @@ -1,3 +1,6 @@ -import './edit_ctrl'; -import './page_ctrl'; -import './list_ctrl'; +import './plugin_edit_ctrl'; +import './plugin_page_ctrl'; +import './plugin_list_ctrl'; +import './import_list/import_list'; +import './ds_edit_ctrl'; +import './ds_list_ctrl'; diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts new file mode 100644 index 00000000000..d2ca7a82dc4 --- /dev/null +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -0,0 +1,146 @@ +/// + +import angular from 'angular'; +import _ from 'lodash'; +import coreModule from 'app/core/core_module'; +import config from 'app/core/config'; + +var datasourceTypes = []; + +var defaults = { + name: '', + type: 'graphite', + url: '', + access: 'proxy', + jsonData: {} +}; + +export class DataSourceEditCtrl { + isNew: boolean; + datasources: any[]; + current: any; + types: any; + testing: any; + datasourceMeta: any; + tabIndex: number; + hasDashboards: boolean; + editForm: any; + + /** @ngInject */ + constructor( + private $scope, + private $q, + private backendSrv, + private $routeParams, + private $location, + private datasourceSrv) { + + this.isNew = true; + this.datasources = []; + this.tabIndex = 0; + + this.loadDatasourceTypes().then(() => { + if (this.$routeParams.id) { + this.getDatasourceById(this.$routeParams.id); + } else { + this.current = angular.copy(defaults); + this.typeChanged(); + } + }); + } + + loadDatasourceTypes() { + if (datasourceTypes.length > 0) { + this.types = datasourceTypes; + return this.$q.when(null); + } + + return this.backendSrv.get('/api/plugins', {enabled: 1, type: 'datasource'}).then(plugins => { + datasourceTypes = plugins; + this.types = plugins; + }); + } + + getDatasourceById(id) { + this.backendSrv.get('/api/datasources/' + id).then(ds => { + this.isNew = false; + this.current = ds; + return this.typeChanged(); + }); + } + + typeChanged() { + this.hasDashboards = false; + return this.backendSrv.get('/api/plugins/' + this.current.type + '/settings').then(pluginInfo => { + this.datasourceMeta = pluginInfo; + this.hasDashboards = _.findWhere(pluginInfo.includes, {type: 'dashboard'}); + }); + } + + updateFrontendSettings() { + return this.backendSrv.get('/api/frontend/settings').then(settings => { + config.datasources = settings.datasources; + config.defaultDatasource = settings.defaultDatasource; + this.datasourceSrv.init(); + }); + } + + testDatasource() { + this.testing = { done: false }; + + this.datasourceSrv.get(this.current.name).then(datasource => { + if (!datasource.testDatasource) { + this.testing.message = 'Data source does not support test connection feature.'; + this.testing.status = 'warning'; + this.testing.title = 'Unknown'; + return; + } + + return datasource.testDatasource().then(result => { + this.testing.message = result.message; + this.testing.status = result.status; + this.testing.title = result.title; + }).catch(err => { + if (err.statusText) { + this.testing.message = err.statusText; + this.testing.title = "HTTP Error"; + } else { + this.testing.message = err.message; + this.testing.title = "Unknown error"; + } + }); + }).finally(() => { + this.testing.done = true; + }); + } + + saveChanges(test) { + if (!this.editForm.$valid) { + return; + } + + if (this.current.id) { + return this.backendSrv.put('/api/datasources/' + this.current.id, this.current).then(() => { + this.updateFrontendSettings().then(() => { + if (test) { + this.testDatasource(); + } + }); + }); + } else { + return this.backendSrv.post('/api/datasources', this.current).then(result => { + this.updateFrontendSettings(); + this.$location.path('datasources/edit/' + result.id); + }); + } + }; +} + +coreModule.controller('DataSourceEditCtrl', DataSourceEditCtrl); + +coreModule.directive('datasourceHttpSettings', function() { + return { + scope: {current: "="}, + templateUrl: 'public/app/features/plugins/partials/ds_http_settings.html' + }; +}); diff --git a/public/app/features/datasources/list_ctrl.ts b/public/app/features/plugins/ds_list_ctrl.ts similarity index 95% rename from public/app/features/datasources/list_ctrl.ts rename to public/app/features/plugins/ds_list_ctrl.ts index b1f93f1a158..d7b08dcd312 100644 --- a/public/app/features/datasources/list_ctrl.ts +++ b/public/app/features/plugins/ds_list_ctrl.ts @@ -37,10 +37,10 @@ export class DataSourcesCtrl { removeDataSource(ds) { this.$scope.appEvent('confirm-modal', { - title: 'Confirm delete datasource', + title: 'Delete', text: 'Are you sure you want to delete datasource ' + ds.name + '?', yesText: "Delete", - icon: "fa-warning", + icon: "fa-trash", onConfirm: () => { this.removeDataSourceConfirmed(ds); } diff --git a/public/app/features/plugins/import_list/import_list.html b/public/app/features/plugins/import_list/import_list.html new file mode 100644 index 00000000000..acb6654d520 --- /dev/null +++ b/public/app/features/plugins/import_list/import_list.html @@ -0,0 +1,37 @@ +
+ + + + + + + + + + +
+ + + + {{dash.title}} + + + {{dash.title}} + + + v{{dash.revision}} + + 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 new file mode 100644 index 00000000000..dd0a8eef524 --- /dev/null +++ b/public/app/features/plugins/import_list/import_list.ts @@ -0,0 +1,69 @@ +/// + +import angular from 'angular'; +import _ from 'lodash'; +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/${this.plugin.id}/dashboards`).then(dashboards => { + this.dashboards = dashboards; + }); + } + + import(dash, reinstall) { + var installCmd = { + pluginId: this.plugin.id, + path: dash.path, + reinstall: reinstall, + inputs: [] + }; + + 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); + }); + } + + remove(dash) { + this.backendSrv.delete('/api/dashboards/' + dash.installedUri).then(() => { + this.$rootScope.appEvent('alert-success', ['Dashboard Deleted', dash.title]); + dash.installed = false; + }); + } +} + +export function dashboardImportList() { + return { + restrict: 'E', + templateUrl: 'public/app/features/plugins/import_list/import_list.html', + controller: DashImportListCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + plugin: "=", + datasource: "=" + } + }; +} + +coreModule.directive('dashboardImportList', dashboardImportList); + + + + diff --git a/public/app/features/plugins/list_ctrl.ts b/public/app/features/plugins/list_ctrl.ts deleted file mode 100644 index a1bacc09c14..00000000000 --- a/public/app/features/plugins/list_ctrl.ts +++ /dev/null @@ -1,17 +0,0 @@ -/// - -import angular from 'angular'; - -export class PluginListCtrl { - plugins: any[]; - - /** @ngInject */ - constructor(private backendSrv: any) { - - this.backendSrv.get('api/org/plugins').then(plugins => { - this.plugins = plugins; - }); - } -} - -angular.module('grafana.controllers').controller('PluginListCtrl', PluginListCtrl); diff --git a/public/app/features/plugins/partials/ds_edit.html b/public/app/features/plugins/partials/ds_edit.html new file mode 100644 index 00000000000..3305bfd67af --- /dev/null +++ b/public/app/features/plugins/partials/ds_edit.html @@ -0,0 +1,81 @@ + + + +
+ + + +
+ +
+
+
+ Name + + + The name is used when you select the data source in panels. + The Default data source is preselected in new + panels. + + + +
+ +
+ Type +
+ +
+
+
+ + + + + + +
+
Testing....
+
Test results
+
+
{{ctrl.testing.title}}
+
+
+
+ +
+ + + + Cancel +
+ +
+
+ +
+ +
+ +
+ diff --git a/public/app/features/datasources/partials/http_settings.html b/public/app/features/plugins/partials/ds_http_settings.html similarity index 100% rename from public/app/features/datasources/partials/http_settings.html rename to public/app/features/plugins/partials/ds_http_settings.html diff --git a/public/app/features/datasources/partials/list.html b/public/app/features/plugins/partials/ds_list.html similarity index 100% rename from public/app/features/datasources/partials/list.html rename to public/app/features/plugins/partials/ds_list.html diff --git a/public/app/features/plugins/partials/edit.html b/public/app/features/plugins/partials/edit.html deleted file mode 100644 index 815e165cc85..00000000000 --- a/public/app/features/plugins/partials/edit.html +++ /dev/null @@ -1,185 +0,0 @@ - - - - Apps - - - -
-
- -
-

{{ctrl.model.name}}

-
By {{ctrl.model.info.author.name}}
-
- - {{ctrl.model.type}} - -
-
-
- - - -
-
- README.md -
- -
- Details -
- -
-
-
- -
-
- -
-
- -
- -
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/app/features/plugins/partials/list.html b/public/app/features/plugins/partials/list.html deleted file mode 100644 index 2b2f9741be0..00000000000 --- a/public/app/features/plugins/partials/list.html +++ /dev/null @@ -1,42 +0,0 @@ - - - -
- - - - - - - - - - - - - - - - - - - -
NameType
- - {{plugin.name}} - - - {{plugin.type}} - - Enabled - Pinned - - - - Edit - -
-
-
diff --git a/public/app/features/plugins/partials/plugin_edit.html b/public/app/features/plugins/partials/plugin_edit.html new file mode 100644 index 00000000000..6ae4ac5f95c --- /dev/null +++ b/public/app/features/plugins/partials/plugin_edit.html @@ -0,0 +1,94 @@ + + + +
+ + +
+
+
+
+
+ +
+
+ + +
+ + +
+
+
+ +
+ +
+ + +
+
diff --git a/public/app/features/plugins/partials/plugin_list.html b/public/app/features/plugins/partials/plugin_list.html new file mode 100644 index 00000000000..5c3d38b3459 --- /dev/null +++ b/public/app/features/plugins/partials/plugin_list.html @@ -0,0 +1,62 @@ + + + +
+ + + + + + + + + + + + + + + + + + + +
NameType
+ + {{plugin.name}} + + + {{plugin.type}} + + Enabled + Pinned + + + + Edit + +
+
+
diff --git a/public/app/features/plugins/partials/page.html b/public/app/features/plugins/partials/plugin_page.html similarity index 100% rename from public/app/features/plugins/partials/page.html rename to public/app/features/plugins/partials/plugin_page.html diff --git a/public/app/features/plugins/edit_ctrl.ts b/public/app/features/plugins/plugin_edit_ctrl.ts similarity index 50% rename from public/app/features/plugins/edit_ctrl.ts rename to public/app/features/plugins/plugin_edit_ctrl.ts index 6d693c19a4e..8e9dc32eeee 100644 --- a/public/app/features/plugins/edit_ctrl.ts +++ b/public/app/features/plugins/plugin_edit_ctrl.ts @@ -5,26 +5,71 @@ import _ from 'lodash'; export class PluginEditCtrl { model: any; + pluginIcon: string; pluginId: any; - includedPanels: any; + includes: any; + readmeHtml: any; includedDatasources: any; tabIndex: number; + tabs: any; + hasDashboards: any; preUpdateHook: () => any; postUpdateHook: () => any; /** @ngInject */ - constructor(private backendSrv: any, private $routeParams: any) { + constructor(private backendSrv, private $routeParams, private $sce, private $http) { this.model = {}; this.pluginId = $routeParams.pluginId; this.tabIndex = 0; + this.tabs = ['Overview']; + } - this.backendSrv.get(`/api/org/plugins/${this.pluginId}/settings`).then(result => { + init() { + return this.backendSrv.get(`/api/plugins/${this.pluginId}/settings`).then(result => { this.model = result; - this.includedPanels = _.where(result.includes, {type: 'panel'}); - this.includedDatasources = _.where(result.includes, {type: 'datasource'}); + this.pluginIcon = this.getPluginIcon(this.model.type); + + this.model.dependencies.plugins.forEach(plug => { + plug.icon = this.getPluginIcon(plug.type); + }); + + this.includes = _.map(result.includes, plug => { + plug.icon = this.getPluginIcon(plug.type); + 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.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)); + }); + }); + } + + getPluginIcon(type) { + switch (type) { + case 'datasource': return 'icon-gf icon-gf-datasources'; + 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'; + } + } + update() { var chain = Promise.resolve(); var self = this; @@ -32,31 +77,27 @@ 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 chain = chain.then(function() { var updateCmd = _.extend({ - pluginId: self.model.pluginId, - orgId: self.model.orgId, enabled: self.model.enabled, pinned: self.model.pinned, jsonData: self.model.jsonData, 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 // the final step of the update procedure (reloading the page) until the promise - // resolves. If the promise is rejected the page will not be reloaded. + // 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(); }); } @@ -71,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 new file mode 100644 index 00000000000..9e9b679c6c1 --- /dev/null +++ b/public/app/features/plugins/plugin_list_ctrl.ts @@ -0,0 +1,34 @@ +/// + +import angular from 'angular'; + +export class PluginListCtrl { + plugins: any[]; + tabIndex: number; + + /** @ngInject */ + constructor(private backendSrv: any, $location) { + this.tabIndex = 0; + + 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; + }); + } +} + +angular.module('grafana.controllers').controller('PluginListCtrl', PluginListCtrl); diff --git a/public/app/features/plugins/page_ctrl.ts b/public/app/features/plugins/plugin_page_ctrl.ts similarity index 88% rename from public/app/features/plugins/page_ctrl.ts rename to public/app/features/plugins/plugin_page_ctrl.ts index 9157f14202e..6a840717a80 100644 --- a/public/app/features/plugins/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/profile/partials/password.html b/public/app/features/profile/partials/password.html index 624730c1ce6..84af3c8c922 100644 --- a/public/app/features/profile/partials/password.html +++ b/public/app/features/profile/partials/password.html @@ -9,17 +9,17 @@
Old Password - +
New Password - +
Confirm Password - +
diff --git a/public/app/features/snapshot/snapshot_ctrl.ts b/public/app/features/snapshot/snapshot_ctrl.ts index 7b38f9ad8eb..7085c058046 100644 --- a/public/app/features/snapshot/snapshot_ctrl.ts +++ b/public/app/features/snapshot/snapshot_ctrl.ts @@ -27,10 +27,10 @@ export class SnapshotsCtrl { removeSnapshot(snapshot) { this.$rootScope.appEvent('confirm-modal', { - title: 'Confirm delete snapshot', + title: 'Delete', text: 'Are you sure you want to delete snapshot ' + snapshot.name + '?', yesText: "Delete", - icon: "fa-warning", + icon: "fa-trash", onConfirm: () => { this.removeSnapshotConfirmed(snapshot); } diff --git a/public/app/features/templating/editorCtrl.js b/public/app/features/templating/editorCtrl.js index 46a3072090d..b2916e1bcc7 100644 --- a/public/app/features/templating/editorCtrl.js +++ b/public/app/features/templating/editorCtrl.js @@ -12,13 +12,19 @@ function (angular, _) { var replacementDefaults = { type: 'query', datasource: null, - refresh: false, + refresh: 0, name: '', options: [], includeAll: false, multi: false, }; + $scope.refreshOptions = [ + {value: 0, text: "Never"}, + {value: 1, text: "On Dashboard Load"}, + {value: 2, text: "On Time Range Change"}, + ]; + $scope.init = function() { $scope.mode = 'list'; diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index 64541868f3b..7bd1fd0940f 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -170,9 +170,9 @@
- Update - - Check if you want values to be updated on dashboard load, will slow down dashboard load time + Refresh + + When to update the values of this variable, will slow down dashboard load / time change
diff --git a/public/app/features/templating/templateSrv.js b/public/app/features/templating/templateSrv.js index 3527c386220..ece69302196 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); } @@ -126,9 +129,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 +137,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/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index 46d2f90bc03..ce26a35be9b 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -13,11 +13,25 @@ function (angular, _, kbn) { function getNoneOption() { return { text: 'None', value: '', isNone: true }; } - $rootScope.onAppEvent('time-range-changed', function() { - var variable = _.findWhere(self.variables, { type: 'interval' }); - if (variable) { - self.updateAutoInterval(variable); + // update time variant variables + $rootScope.onAppEvent('refresh', function() { + + // look for interval variables + var intervalVariable = _.findWhere(self.variables, { type: 'interval' }); + if (intervalVariable) { + self.updateAutoInterval(intervalVariable); } + + // update variables with refresh === 2 + var promises = self.variables + .filter(function(variable) { + return variable.refresh === 2; + }).map(function(variable) { + return self.updateOptions(variable); + }); + + return $q.all(promises); + }, $rootScope); this.init = function(dashboard) { @@ -27,29 +41,71 @@ function (angular, _, kbn) { var queryParams = $location.search(); var promises = []; + // use promises to delay processing variables that + // depend on other variables. + this.variableLock = {}; + _.forEach(this.variables, function(variable) { + self.variableLock[variable.name] = $q.defer(); + }); + for (var i = 0; i < this.variables.length; i++) { var variable = this.variables[i]; - var urlValue = queryParams['var-' + variable.name]; - if (urlValue !== void 0) { - promises.push(this.setVariableFromUrl(variable, urlValue)); - } - else if (variable.refresh) { - promises.push(this.updateOptions(variable)); - } - else if (variable.type === 'interval') { - this.updateAutoInterval(variable); - } + promises.push(this.processVariable(variable, queryParams)); } return $q.all(promises); }; - this.setVariableFromUrl = function(variable, urlValue) { - var option = _.findWhere(variable.options, { text: urlValue }); - option = option || { text: urlValue, value: urlValue }; + this.processVariable = function(variable, queryParams) { + var dependencies = []; + var lock = self.variableLock[variable.name]; - this.updateAutoInterval(variable); - return this.setVariableValue(variable, option); + // determine our dependencies. + if (variable.type === "query") { + _.forEach(this.variables, function(v) { + if (templateSrv.containsVariable(variable.query, v.name)) { + dependencies.push(self.variableLock[v.name].promise); + } + }); + } + + return $q.all(dependencies).then(function() { + var urlValue = queryParams['var-' + variable.name]; + if (urlValue !== void 0) { + return self.setVariableFromUrl(variable, urlValue).then(lock.resolve); + } + else if (variable.refresh === 1 || variable.refresh === 2) { + return self.updateOptions(variable).then(function() { + if (_.isEmpty(variable.current) && variable.options.length) { + console.log("setting current for %s", variable.name); + self.setVariableValue(variable, variable.options[0]); + } + lock.resolve(); + }); + } + else if (variable.type === 'interval') { + self.updateAutoInterval(variable); + lock.resolve(); + } else { + lock.resolve(); + } + }); + }; + + this.setVariableFromUrl = function(variable, urlValue) { + var promise = $q.when(true); + + if (variable.refresh) { + promise = this.updateOptions(variable); + } + + return promise.then(function() { + var option = _.findWhere(variable.options, { text: urlValue }); + option = option || { text: urlValue, value: urlValue }; + + self.updateAutoInterval(variable); + return self.setVariableValue(variable, option, true); + }); }; this.updateAutoInterval = function(variable) { @@ -64,7 +120,7 @@ function (angular, _, kbn) { templateSrv.setGrafanaVariable('$__auto_interval', interval); }; - this.setVariableValue = function(variable, option) { + this.setVariableValue = function(variable, option, initPhase) { variable.current = angular.copy(option); if (_.isArray(variable.current.value)) { @@ -72,8 +128,14 @@ function (angular, _, kbn) { } self.selectOptionsForCurrentValue(variable); - templateSrv.updateTemplateData(); + + // on first load, variable loading is ordered to ensure + // that parents are updated before children. + if (initPhase) { + return $q.when(); + } + return self.updateOptionsInChildVariables(variable); }; @@ -145,7 +207,7 @@ function (angular, _, kbn) { this.validateVariableSelectionState = function(variable) { if (!variable.current) { if (!variable.options.length) { return; } - return self.setVariableValue(variable, variable.options[0]); + return self.setVariableValue(variable, variable.options[0], true); } if (_.isArray(variable.current.value)) { @@ -153,7 +215,7 @@ function (angular, _, kbn) { } else { var currentOption = _.findWhere(variable.options, { text: variable.current.text }); if (currentOption) { - return self.setVariableValue(variable, currentOption); + return self.setVariableValue(variable, currentOption, true); } else { if (!variable.options.length) { return; } return self.setVariableValue(variable, variable.options[0]); diff --git a/public/app/partials/confirm_modal.html b/public/app/partials/confirm_modal.html index 24802814be1..d9da8bdacc4 100644 --- a/public/app/partials/confirm_modal.html +++ b/public/app/partials/confirm_modal.html @@ -1,24 +1,30 @@