Commit c5a81719 by Torkel Ödegaard

more work on dashboard importing and templating

parent d2b0bad1
......@@ -176,7 +176,7 @@ func Register(r *macaron.Macaron) {
r.Get("/", wrap(GetPluginList))
r.Get("/dashboards/:pluginId", wrap(GetPluginDashboards))
r.Post("/dashboards/install", bind(dtos.InstallPluginDashboardCmd{}), wrap(InstallPluginDashboard))
r.Post("/dashboards/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
r.Get("/:pluginId/settings", wrap(GetPluginSettingById))
r.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting))
......
......@@ -26,9 +26,9 @@ type PluginListItem struct {
Info *plugins.PluginInfo `json:"info"`
}
type InstallPluginDashboardCmd struct {
type ImportDashboardCommand struct {
PluginId string `json:"pluginId"`
Path string `json:"path"`
Reinstall bool `json:"reinstall"`
Inputs map[string]interface{} `json:"inputs"`
Inputs []plugins.ImportDashboardInput `json:"inputs"`
}
......@@ -122,9 +122,9 @@ func GetPluginDashboards(c *middleware.Context) Response {
}
}
func InstallPluginDashboard(c *middleware.Context, apiCmd dtos.InstallPluginDashboardCmd) Response {
func ImportDashboard(c *middleware.Context, apiCmd dtos.ImportDashboardCommand) Response {
cmd := plugins.InstallPluginDashboardCommand{
cmd := plugins.ImportDashboardCommand{
OrgId: c.OrgId,
UserId: c.UserId,
PluginId: apiCmd.PluginId,
......
// 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")
})
}
......@@ -2,12 +2,14 @@ package plugins
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/dynmap"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
)
type InstallPluginDashboardCommand struct {
type ImportDashboardCommand struct {
Path string `json:"string"`
Inputs map[string]interface{} `json:"inputs"`
Inputs []ImportDashboardInput `json:"inputs"`
OrgId int64 `json:"-"`
UserId int64 `json:"-"`
......@@ -15,11 +17,18 @@ type InstallPluginDashboardCommand struct {
Result *PluginDashboardInfoDTO
}
type ImportDashboardInput struct {
Type string `json:"type"`
PluginId string `json:"pluginId"`
Name string `json:"name"`
Value string `json:"value"`
}
func init() {
bus.AddHandler("plugins", InstallPluginDashboard)
bus.AddHandler("plugins", ImportDashboard)
}
func InstallPluginDashboard(cmd *InstallPluginDashboardCommand) error {
func ImportDashboard(cmd *ImportDashboardCommand) error {
plugin, exists := Plugins[cmd.PluginId]
if !exists {
......@@ -55,3 +64,67 @@ func InstallPluginDashboard(cmd *InstallPluginDashboardCommand) error {
return nil
}
type DashTemplateEvaluator struct {
template *dynmap.Object
inputs []ImportDashboardInput
variables map[string]string
result *dynmap.Object
}
// func (this *DashTemplateEvaluator) getObject(path string) map[string]interface{} {
// if obj, exists := this.template[path]; exists {
// return obj.(map[string]interface{})
// }
// return nil
// }
func (this *DashTemplateEvaluator) findInput(varName string, varDef *dynmap.Object) *ImportDashboardInput {
inputType, _ := varDef.GetString("type")
for _, input := range this.inputs {
if inputType == input.Type && (input.Name == varName || input.Name == "*") {
return &input
}
}
return nil
}
func (this *DashTemplateEvaluator) Eval() (*dynmap.Object, error) {
this.result = dynmap.NewObject()
this.variables = make(map[string]string)
// check that we have all inputs we need
if requiredInputs, err := this.template.GetObject("__inputs"); err == nil {
for varName, value := range requiredInputs.Map() {
varDef, _ := value.Object()
input := this.findInput(varName, varDef)
this.variables[varName] = input.Value
}
}
this.EvalObject(this.template, this.result)
return this.result, nil
}
func (this *DashTemplateEvaluator) EvalObject(source *dynmap.Object, writer *dynmap.Object) {
for key, value := range source.Map() {
if key == "__input" {
continue
}
goValue := value.Interface()
switch goValue.(type) {
case string:
writer.SetValue(key, goValue)
case map[string]interface{}:
childSource, _ := value.Object()
childWriter, _ := writer.SetValue(key, map[string]interface{}{}).Object()
this.EvalObject(childSource, childWriter)
default:
log.Error(3, "Unknown json type key: %v , type: %v", key, goValue)
}
}
}
package plugins
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/dynmap"
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{},
}
err = ImportDashboard(&cmd)
So(err, ShouldBeNil)
Convey("should install dashboard", func() {
So(importedDash, ShouldNotBeNil)
rows := importedDash.Data["rows"].([]interface{})
row1 := rows[0].(map[string]interface{})
panels := row1["panels"].([]interface{})
panel := panels[0].(map[string]interface{})
So(panel["datasource"], ShouldEqual, "graphite")
So(importedDash.Data["__inputs"], ShouldBeNil)
})
})
Convey("When evaling dashboard template", t, func() {
template, _ := dynmap.NewObjectFromBytes([]byte(`{
"__input": {
"graphite": {
"type": "datasource"
}
},
"test": {
"prop": "$__graphite"
}
}`))
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.MustGetString("test.prop", ""), ShouldEqual, "my-server")
})
})
}
......@@ -7,6 +7,7 @@ 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 = [];
......@@ -21,9 +22,18 @@ export class DashImportListCtrl {
pluginId: this.plugin.id,
path: dash.path,
reinstall: reinstall,
inputs: {}
inputs: []
};
if (this.datasource) {
installCmd.inputs.push({
name: '*',
type: 'datasource',
pluginId: this.datasource.type,
value: this.datasource.name
});
}
this.backendSrv.post(`/api/plugins/dashboards/install`, installCmd).then(res => {
this.$rootScope.appEvent('alert-success', ['Dashboard Installed', dash.title]);
_.extend(dash, res);
......@@ -46,7 +56,8 @@ export function dashboardImportList() {
bindToController: true,
controllerAs: 'ctrl',
scope: {
plugin: "="
plugin: "=",
datasource: "="
}
};
}
......
......@@ -74,7 +74,7 @@
</div>
<div ng-if="ctrl.tabIndex === 1" class="tab-content">
<dashboard-import-list plugin="ctrl.datasourceMeta"></dashboard-import-list>
<dashboard-import-list plugin="ctrl.datasourceMeta" datasource="ctrl.current" ></dashboard-import-list>
</div>
</div>
......
......@@ -2,6 +2,7 @@
"__inputs": {
"graphite": {
"type": "datasource",
"pluginId": "graphite",
"description": "Graphite datasource"
}
},
......
{
"__inputs": {
"graphite": {
"type": "datasource",
"pluginId": "graphite",
"description": "Graphite datasource"
}
},
"title": "Nginx Connections",
"revision": "1.5",
"schemaVersion": 11
"schemaVersion": 11,
"rows": [
{
"panels": [
{
"type": "graph",
"datasource": "$__graphite"
}
]
}
]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment