Merge branch 'master' into preferences
This commit is contained in:
+13
-5
@@ -127,10 +127,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
|
||||
@@ -172,7 +168,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)
|
||||
@@ -186,6 +193,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
|
||||
|
||||
@@ -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{}{}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+52
-30
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
+22
-17
@@ -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 {
|
||||
|
||||
+39
-17
@@ -3,24 +3,46 @@ 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 PluginList []PluginListItem
|
||||
|
||||
func (slice PluginList) Len() int {
|
||||
return len(slice)
|
||||
}
|
||||
|
||||
func (slice PluginList) Less(i, j int) bool {
|
||||
return slice[i].Name < slice[j].Name
|
||||
}
|
||||
|
||||
func (slice PluginList) Swap(i, j int) {
|
||||
slice[i], slice[j] = slice[j], slice[i]
|
||||
}
|
||||
|
||||
type ImportDashboardCommand struct {
|
||||
PluginId string `json:"pluginId"`
|
||||
Path string `json:"path"`
|
||||
Reinstall bool `json:"reinstall"`
|
||||
Inputs []plugins.ImportDashboardInput `json:"inputs"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+16
-11
@@ -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 {
|
||||
|
||||
+4
-2
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
func GetPluginList(c *middleware.Context) Response {
|
||||
pluginSettingsMap, err := plugins.GetPluginSettings(c.OrgId)
|
||||
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to get list of plugins", err)
|
||||
}
|
||||
|
||||
result := make([]*dtos.PluginListItem, 0)
|
||||
for _, pluginDef := range plugins.Plugins {
|
||||
listItem := &dtos.PluginListItem{
|
||||
PluginId: pluginDef.Id,
|
||||
Name: pluginDef.Name,
|
||||
Type: pluginDef.Type,
|
||||
Info: &pluginDef.Info,
|
||||
}
|
||||
|
||||
if pluginSetting, exists := pluginSettingsMap[pluginDef.Id]; exists {
|
||||
listItem.Enabled = pluginSetting.Enabled
|
||||
listItem.Pinned = pluginSetting.Pinned
|
||||
}
|
||||
|
||||
result = append(result, listItem)
|
||||
}
|
||||
|
||||
return Json(200, result)
|
||||
}
|
||||
|
||||
func GetPluginSettingById(c *middleware.Context) Response {
|
||||
pluginId := c.Params(":pluginId")
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err != m.ErrPluginSettingNotFound {
|
||||
return ApiError(500, "Failed to get login settings", nil)
|
||||
}
|
||||
} else {
|
||||
dto.Enabled = query.Result.Enabled
|
||||
dto.Pinned = query.Result.Pinned
|
||||
dto.JsonData = query.Result.JsonData
|
||||
}
|
||||
|
||||
return Json(200, dto)
|
||||
}
|
||||
}
|
||||
|
||||
func UpdatePluginSetting(c *middleware.Context, cmd m.UpdatePluginSettingCmd) Response {
|
||||
pluginId := c.Params(":pluginId")
|
||||
|
||||
cmd.OrgId = c.OrgId
|
||||
cmd.PluginId = pluginId
|
||||
|
||||
if _, ok := plugins.Apps[cmd.PluginId]; !ok {
|
||||
return ApiError(404, "Plugin not installed.", nil)
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to update plugin setting", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Plugin settings updated")
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
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 {
|
||||
return ApiError(500, "Failed to get list of plugins", err)
|
||||
}
|
||||
|
||||
result := make(dtos.PluginList, 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{
|
||||
Id: pluginDef.Id,
|
||||
Name: pluginDef.Name,
|
||||
Type: pluginDef.Type,
|
||||
Info: &pluginDef.Info,
|
||||
}
|
||||
|
||||
if pluginSetting, exists := pluginSettingsMap[pluginDef.Id]; exists {
|
||||
listItem.Enabled = pluginSetting.Enabled
|
||||
listItem.Pinned = pluginSetting.Pinned
|
||||
}
|
||||
|
||||
// filter out disabled
|
||||
if enabledFilter == "1" && !listItem.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
// filter out built in data sources
|
||||
if ds, exists := plugins.DataSources[pluginDef.Id]; exists {
|
||||
if ds.BuiltIn {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, listItem)
|
||||
}
|
||||
|
||||
sort.Sort(result)
|
||||
return Json(200, result)
|
||||
}
|
||||
|
||||
func GetPluginSettingById(c *middleware.Context) Response {
|
||||
pluginId := c.Params(":pluginId")
|
||||
|
||||
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,
|
||||
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
|
||||
}
|
||||
|
||||
query := m.GetPluginSettingByIdQuery{PluginId: pluginId, OrgId: c.OrgId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err != m.ErrPluginSettingNotFound {
|
||||
return ApiError(500, "Failed to get login settings", nil)
|
||||
}
|
||||
} else {
|
||||
dto.Enabled = query.Result.Enabled
|
||||
dto.Pinned = query.Result.Pinned
|
||||
dto.JsonData = query.Result.JsonData
|
||||
}
|
||||
|
||||
return Json(200, dto)
|
||||
}
|
||||
}
|
||||
|
||||
func UpdatePluginSetting(c *middleware.Context, cmd m.UpdatePluginSettingCmd) Response {
|
||||
pluginId := c.Params(":pluginId")
|
||||
|
||||
cmd.OrgId = c.OrgId
|
||||
cmd.PluginId = pluginId
|
||||
|
||||
if _, ok := plugins.Apps[cmd.PluginId]; !ok {
|
||||
return ApiError(404, "Plugin not installed.", nil)
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to update plugin setting", err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 + "\"")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Type>(..) 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 Get<Type>Array() 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,
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
+22
-29
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
+29
-27
@@ -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:"-"`
|
||||
|
||||
@@ -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 {
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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, "")
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -3,41 +3,44 @@ package plugins
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
type FrontendPluginBase struct {
|
||||
PluginBase
|
||||
Module string `json:"module"`
|
||||
BaseUrl string `json:"baseUrl"`
|
||||
StaticRoot string `json:"staticRoot"`
|
||||
StaticRootAbs string `json:"-"`
|
||||
}
|
||||
|
||||
func (fp *FrontendPluginBase) initFrontendPlugin() {
|
||||
if fp.StaticRoot != "" {
|
||||
fp.StaticRootAbs = filepath.Join(fp.PluginDir, fp.StaticRoot)
|
||||
if isInternalPlugin(fp.PluginDir) {
|
||||
StaticRoutes = append(StaticRoutes, &PluginStaticRoute{
|
||||
Directory: fp.StaticRootAbs,
|
||||
Directory: fp.PluginDir,
|
||||
PluginId: fp.Id,
|
||||
})
|
||||
}
|
||||
|
||||
fp.handleModuleDefaults()
|
||||
|
||||
fp.Info.Logos.Small = evalRelativePluginUrlPath(fp.Info.Logos.Small, fp.Id)
|
||||
fp.Info.Logos.Large = evalRelativePluginUrlPath(fp.Info.Logos.Large, fp.Id)
|
||||
fp.Info.Logos.Small = getPluginLogoUrl(fp.Info.Logos.Small, fp.BaseUrl)
|
||||
fp.Info.Logos.Large = getPluginLogoUrl(fp.Info.Logos.Large, fp.BaseUrl)
|
||||
|
||||
for i := 0; i < len(fp.Info.Screenshots); i++ {
|
||||
fp.Info.Screenshots[i].Path = evalRelativePluginUrlPath(fp.Info.Screenshots[i].Path, fp.Id)
|
||||
fp.Info.Screenshots[i].Path = evalRelativePluginUrlPath(fp.Info.Screenshots[i].Path, fp.BaseUrl)
|
||||
}
|
||||
}
|
||||
|
||||
func getPluginLogoUrl(path, baseUrl string) string {
|
||||
if path == "" {
|
||||
return "public/img/plugin-default-logo_dark.svg"
|
||||
}
|
||||
|
||||
return evalRelativePluginUrlPath(path, baseUrl)
|
||||
}
|
||||
|
||||
func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) {
|
||||
appSubPath := strings.Replace(fp.PluginDir, app.StaticRootAbs, "", 1)
|
||||
appSubPath := strings.Replace(fp.PluginDir, app.PluginDir, "", 1)
|
||||
fp.IncludedInAppId = app.Id
|
||||
fp.BaseUrl = app.BaseUrl
|
||||
fp.Module = util.JoinUrlFragments("plugins/"+app.Id, appSubPath) + "/module"
|
||||
@@ -45,7 +48,7 @@ func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) {
|
||||
|
||||
func (fp *FrontendPluginBase) handleModuleDefaults() {
|
||||
|
||||
if fp.StaticRoot != "" {
|
||||
if isInternalPlugin(fp.PluginDir) {
|
||||
fp.Module = path.Join("plugins", fp.Id, "module")
|
||||
fp.BaseUrl = path.Join("public/plugins", fp.Id)
|
||||
return
|
||||
@@ -55,7 +58,11 @@ func (fp *FrontendPluginBase) handleModuleDefaults() {
|
||||
fp.BaseUrl = path.Join("public/app/plugins", fp.Type, fp.Id)
|
||||
}
|
||||
|
||||
func evalRelativePluginUrlPath(pathStr string, pluginId string) string {
|
||||
func isInternalPlugin(pluginDir string) bool {
|
||||
return !strings.Contains(pluginDir, setting.StaticRootPath)
|
||||
}
|
||||
|
||||
func evalRelativePluginUrlPath(pathStr string, baseUrl string) string {
|
||||
if pathStr == "" {
|
||||
return ""
|
||||
}
|
||||
@@ -64,5 +71,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)
|
||||
}
|
||||
|
||||
+54
-4
@@ -3,24 +3,47 @@ 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"`
|
||||
|
||||
IncludedInAppId string `json:"-"`
|
||||
PluginDir string `json:"-"`
|
||||
|
||||
// cache for readme file contents
|
||||
Readme []byte `json:"-"`
|
||||
}
|
||||
|
||||
func (pb *PluginBase) registerPlugin(pluginDir string) error {
|
||||
@@ -32,11 +55,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"`
|
||||
|
||||
@@ -3,6 +3,7 @@ package plugins
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -155,3 +156,31 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
|
||||
reader.Seek(0, 0)
|
||||
return loader.Load(jsonParser, currentDir)
|
||||
}
|
||||
|
||||
func GetPluginReadme(pluginId string) ([]byte, error) {
|
||||
plug, exists := Plugins[pluginId]
|
||||
if !exists {
|
||||
return nil, PluginNotFoundError{pluginId}
|
||||
}
|
||||
|
||||
if plug.Readme != nil {
|
||||
return plug.Readme, nil
|
||||
}
|
||||
|
||||
readmePath := filepath.Join(plug.PluginDir, "README.md")
|
||||
if _, err := os.Stat(readmePath); os.IsNotExist(err) {
|
||||
readmePath = filepath.Join(plug.PluginDir, "readme.md")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(readmePath); os.IsNotExist(err) {
|
||||
plug.Readme = make([]byte, 0)
|
||||
return plug.Readme, nil
|
||||
}
|
||||
|
||||
if readmeBytes, err := ioutil.ReadFile(readmePath); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
plug.Readme = readmeBytes
|
||||
return plug.Readme, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
+32
-22
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -150,7 +150,7 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
|
||||
sql.WriteString(" AND (")
|
||||
for i, dashboardId := range query.DashboardIds {
|
||||
if i != 0 {
|
||||
sql.WriteString("OR")
|
||||
sql.WriteString(" OR")
|
||||
}
|
||||
|
||||
sql.WriteString(" dashboard.id = ?")
|
||||
@@ -167,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
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
)
|
||||
@@ -12,11 +13,11 @@ import (
|
||||
func insertTestDashboard(title string, orgId int64, tags ...interface{}) *m.Dashboard {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: orgId,
|
||||
Dashboard: map[string]interface{}{
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": title,
|
||||
"tags": tags,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
@@ -58,11 +59,11 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Overwrite: true,
|
||||
Dashboard: map[string]interface{}{
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": float64(123412321),
|
||||
"title": "Expect error",
|
||||
"tags": []interface{}{},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
@@ -76,11 +77,11 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 2,
|
||||
Overwrite: true,
|
||||
Dashboard: map[string]interface{}{
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": float64(query.Result.Id),
|
||||
"title": "Expect error",
|
||||
"tags": []interface{}{},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
@@ -135,11 +136,11 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
Convey("Should not be able to save dashboard with same name", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: map[string]interface{}{
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dash 23",
|
||||
"tags": []interface{}{},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user