new endpoint to post/get trimmed dashboard json (#33465)

* new endpoint to post/get trimed dashboard json

* add isdisabled check in dashboard.go
This commit is contained in:
ying-jeanne 2021-04-28 19:38:33 +08:00 committed by GitHub
parent 076e2ce06a
commit 748778fff0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 572 additions and 14 deletions

View File

@ -80,6 +80,7 @@ Family: scuemata.#Family & {
// synthetic Family to represent them in Go, for ease of generating
// e.g. JSON Schema.
#Panel: {
...
// The panel plugin type id.
type: !=""
@ -91,7 +92,7 @@ Family: scuemata.#Family & {
// _pv: { maj: int, min: int }
// The major and minor versions of the panel plugin for this schema.
// TODO 2-tuple list instead of struct?
panelSchema: { maj: number, min: number }
panelSchema?: { maj: number, min: number }
// Panel title.
title?: string
@ -128,7 +129,7 @@ Family: scuemata.#Family & {
// with types derived from plugins in the Instance variant.
// When working directly from CUE, importers can extend this
// type directly to achieve the same effect.
targets?: [...{}]
targets?: [...{...}]
// The values depend on panel type
options: {...}

3
go.mod
View File

@ -13,7 +13,7 @@ replace k8s.io/client-go => k8s.io/client-go v0.18.8
require (
cloud.google.com/go/storage v1.14.0
cuelang.org/go v0.3.0-beta.6
cuelang.org/go v0.3.2
github.com/BurntSushi/toml v0.3.1
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f
github.com/aws/aws-sdk-go v1.38.17
@ -95,6 +95,7 @@ require (
golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324
golang.org/x/tools v0.1.0 // indirect
gonum.org/v1/gonum v0.9.1
google.golang.org/api v0.45.0
google.golang.org/grpc v1.37.0

4
go.sum
View File

@ -53,6 +53,8 @@ contrib.go.opencensus.io/exporter/ocagent v0.6.0/go.mod h1:zmKjrJcdo0aYcVS7bmEeS
contrib.go.opencensus.io/exporter/prometheus v0.2.0/go.mod h1:TYmVAyE8Tn1lyPcltF5IYYfWp2KHu7lQGIZnj8iZMys=
cuelang.org/go v0.3.0-beta.6 h1:od1S/Hbl2S45TLSONl95X3O4TXN1za6CUSD13bTxCVk=
cuelang.org/go v0.3.0-beta.6/go.mod h1:Ikvs157igkGV5gFUdYSFa+lWp/CDteVhubPTXyvPRtA=
cuelang.org/go v0.3.2 h1:/Am5yFDwqnaEi+g942OPM1M4/qtfVSm49wtkQbeh5Z4=
cuelang.org/go v0.3.2/go.mod h1:jvMO35Q4D2D3m2ujAmKESICaYkjMbu5+D+2zIGuWTpQ=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
@ -1333,6 +1335,7 @@ github.com/pierrec/lz4 v2.6.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi
github.com/pierrec/lz4/v4 v4.0.2-0.20200813132121-22f5d580d5c4/go.mod h1:vvUajMAuienWCEdMnA5Zb5mp0VIa9M8VvKcVEOkoAh8=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -1450,6 +1453,7 @@ github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.2-0.20200830194709-1115b6af0369 h1:wdCVGtPadWC/ZuuLC7Hv58VQ5UF7V98ewE71n5mJfrM=
github.com/rogpeppe/go-internal v1.6.2-0.20200830194709-1115b6af0369/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=

View File

@ -324,6 +324,7 @@ func (hs *HTTPServer) registerRoutes() {
dashboardRoute.Delete("/db/:slug", routing.Wrap(hs.DeleteDashboardBySlug))
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), routing.Wrap(CalculateDashboardDiff))
dashboardRoute.Post("/trim", bind(models.TrimDashboardCommand{}), routing.Wrap(hs.TrimDashboard))
dashboardRoute.Post("/db", bind(models.SaveDashboardCommand{}), routing.Wrap(hs.PostDashboard))
dashboardRoute.Get("/home", routing.Wrap(hs.GetHomeDashboard))

View File

@ -46,9 +46,32 @@ func dashboardGuardianResponse(err error) response.Response {
return response.Error(403, "Access denied to this dashboard", nil)
}
func (hs *HTTPServer) TrimDashboard(c *models.ReqContext, cmd models.TrimDashboardCommand) response.Response {
var err error
dash := cmd.Dashboard
meta := cmd.Meta
trimedResult := *dash
if !hs.LoadSchemaService.IsDisabled() {
trimedResult, err = hs.LoadSchemaService.DashboardTrimDefaults(*dash)
if err != nil {
return response.Error(500, "Error while trim default value from dashboard json", err)
}
}
dto := dtos.TrimDashboardFullWithMeta{
Dashboard: &trimedResult,
Meta: meta,
}
c.TimeRequest(metrics.MApiDashboardGet)
return response.JSON(200, dto)
}
func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
slug := c.Params(":slug")
uid := c.Params(":uid")
trimDefaults := c.QueryBoolWithDefault("trimdefaults", false)
dash, rsp := getDashboardHelper(c.OrgId, slug, 0, uid)
if rsp != nil {
return rsp
@ -154,6 +177,14 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
return response.Error(500, "Error while loading library panels", err)
}
}
var trimedJson simplejson.Json
if trimDefaults && !hs.LoadSchemaService.IsDisabled() {
trimedJson, err = hs.LoadSchemaService.DashboardTrimDefaults(*dash.Data)
if err != nil {
return response.Error(500, "Error while trim default value from dashboard json", err)
}
dash.Data = &trimedJson
}
dto := dtos.DashboardFullWithMeta{
Dashboard: dash.Data,
@ -254,11 +285,17 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response {
}
func (hs *HTTPServer) PostDashboard(c *models.ReqContext, cmd models.SaveDashboardCommand) response.Response {
var err error
cmd.OrgId = c.OrgId
cmd.UserId = c.UserId
trimDefaults := c.QueryBoolWithDefault("trimdefaults", false)
if trimDefaults && !hs.LoadSchemaService.IsDisabled() {
cmd.Dashboard, err = hs.LoadSchemaService.DashboardApplyDefaults(cmd.Dashboard)
if err != nil {
return response.Error(500, "Error while applying default value to the dashboard json", err)
}
}
dash := cmd.GetDashboardModel()
newDashboard := dash.Id == 0 && dash.Uid == ""
if newDashboard {
limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard")

View File

@ -37,6 +37,11 @@ type DashboardFullWithMeta struct {
Dashboard *simplejson.Json `json:"dashboard"`
}
type TrimDashboardFullWithMeta struct {
Meta *simplejson.Json `json:"meta"`
Dashboard *simplejson.Json `json:"dashboard"`
}
type DashboardRedirect struct {
RedirectUri string `json:"redirectUri"`
}

View File

@ -41,6 +41,7 @@ import (
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/schemaloader"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/shorturls"
"github.com/grafana/grafana/pkg/services/sqlstore"
@ -98,6 +99,7 @@ type HTTPServer struct {
DataService *tsdb.Service `inject:""`
PluginDashboardService *plugindashboards.Service `inject:""`
AlertEngine *alerting.AlertEngine `inject:""`
LoadSchemaService *schemaloader.SchemaLoaderService `inject:""`
Listener net.Listener
}

View File

@ -346,6 +346,12 @@ type SaveDashboardCommand struct {
Result *Dashboard
}
type TrimDashboardCommand struct {
Dashboard *simplejson.Json `json:"dashboard" binding:"Required"`
Meta *simplejson.Json `json:"meta"`
Result *Dashboard
}
type DashboardProvisioning struct {
Id int64
DashboardId int64

View File

@ -0,0 +1,116 @@
package load
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"testing"
"cuelang.org/go/cue"
"github.com/google/go-cmp/cmp"
"github.com/grafana/grafana/pkg/schema"
"golang.org/x/tools/txtar"
)
var CasesDir = filepath.Join("testdata", "artifacts", "dashboards", "trimdefault")
type Case struct {
Name string
CUE string
Full string
Trimed string
}
func TestGenerate(t *testing.T) {
cases, err := loadCases(CasesDir)
if err != nil {
t.Fatal(err)
}
for _, c := range cases {
t.Run(c.Name+" apply default value", func(t *testing.T) {
var r cue.Runtime
scmInstance, err := r.Compile(c.Name+".cue", c.CUE)
if err != nil {
t.Fatal(err)
}
inputResource := schema.Resource{Value: c.Trimed}
scm := genericVersionedSchema{actual: scmInstance.Value()}
out, err := scm.ApplyDefaults(inputResource)
if err != nil {
t.Fatal(err)
}
b := []byte(out.Value.(string))
if s := cmp.Diff(string(b), c.Full); s != "" {
t.Fatal(s)
}
})
}
t.Skip()
for _, c := range cases {
t.Run(c.Name+" trim default value", func(t *testing.T) {
var r cue.Runtime
scmInstance, err := r.Compile(c.Name+".cue", c.CUE)
if err != nil {
t.Fatal(err)
}
inputResource := schema.Resource{Value: c.Full}
scm := genericVersionedSchema{actual: scmInstance.Value()}
out, err := scm.TrimDefaults(inputResource)
if err != nil {
t.Fatal(err)
}
b := []byte(out.Value.(string))
if s := cmp.Diff(string(b), c.Trimed); s != "" {
t.Fatal(s)
}
})
}
}
func loadCases(dir string) ([]Case, error) {
files, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
var cases []Case
for _, fi := range files {
file := filepath.Join(dir, fi.Name())
a, err := txtar.ParseFile(file)
if err != nil {
return nil, err
}
if len(a.Files) != 3 {
return nil, fmt.Errorf("Malformed test case '%s': Must contain exactly three files (CUE, Full and Trimed), but has %d", file, len(a.Files))
}
fullBuffer := new(bytes.Buffer)
fullJson := a.Files[1].Data
if err := json.Compact(fullBuffer, fullJson); err != nil {
return nil, err
}
trimBuffer := new(bytes.Buffer)
trimedJson := a.Files[2].Data
if err := json.Compact(trimBuffer, trimedJson); err != nil {
return nil, err
}
cases = append(cases, Case{
Name: fi.Name(),
CUE: string(a.Files[0].Data),
Full: fullBuffer.String(),
Trimed: trimBuffer.String(),
})
}
return cases, nil
}

View File

@ -84,7 +84,8 @@ func DistDashboardFamily(p BaseLoadPaths) (schema.VersionedCueSchema, error) {
#Panel: obj
}
`)
filled := dummy.Value().Fill(dj, "obj")
filled := dummy.Value().FillPath(cue.MakePath(cue.Str("obj")), dj)
ddj := filled.LookupPath(cue.MakePath(cue.Str("dummy")))
var first, prev *compositeDashboardSchema

View File

@ -1,8 +1,12 @@
package load
import (
"bytes"
"fmt"
"cuelang.org/go/cue"
"cuelang.org/go/cue/load"
cuejson "cuelang.org/go/pkg/encoding/json"
"github.com/grafana/grafana/pkg/schema"
)
@ -106,15 +110,119 @@ func (gvs *genericVersionedSchema) Validate(r schema.Resource) error {
// ApplyDefaults returns a new, concrete copy of the Resource with all paths
// that are 1) missing in the Resource AND 2) specified by the schema,
// filled with default values specified by the schema.
func (gvs *genericVersionedSchema) ApplyDefaults(_ schema.Resource) (schema.Resource, error) {
panic("not implemented") // TODO: Implement
func (gvs *genericVersionedSchema) ApplyDefaults(r schema.Resource) (schema.Resource, error) {
rv, err := rt.Compile("resource", r.Value)
if err != nil {
return r, err
}
rvUnified := rv.Value().Unify(gvs.CUE())
re, err := convertCUEValueToString(rvUnified)
if err != nil {
return r, err
}
return schema.Resource{Value: re}, nil
}
func convertCUEValueToString(inputCUE cue.Value) (string, error) {
re, err := cuejson.Marshal(inputCUE)
if err != nil {
return re, err
}
result := []byte(re)
result = bytes.Replace(result, []byte("\\u003c"), []byte("<"), -1)
result = bytes.Replace(result, []byte("\\u003e"), []byte(">"), -1)
result = bytes.Replace(result, []byte("\\u0026"), []byte("&"), -1)
return string(result), nil
}
// TrimDefaults returns a new, concrete copy of the Resource where all paths
// in the where the values at those paths are the same as the default value
// given in the schema.
func (gvs *genericVersionedSchema) TrimDefaults(_ schema.Resource) (schema.Resource, error) {
panic("not implemented") // TODO: Implement
func (gvs *genericVersionedSchema) TrimDefaults(r schema.Resource) (schema.Resource, error) {
rvInstance, err := rt.Compile("resource", r.Value)
if err != nil {
return r, err
}
rv, _, err := removeDefaultHelper(gvs.CUE(), rvInstance.Value())
if err != nil {
return r, err
}
re, err := convertCUEValueToString(rv)
if err != nil {
return r, err
}
return schema.Resource{Value: re}, nil
}
func removeDefaultHelper(inputdef cue.Value, input cue.Value) (cue.Value, bool, error) {
// Since for now, panel definition is open validation,
// we need to loop on the input CUE for trimming
rvInstance, err := rt.Compile("resource", []byte{})
if err != nil {
return input, false, err
}
rv := rvInstance.Value()
switch inputdef.IncompleteKind() {
case cue.StructKind:
// Get all fields including optional fields
iter, err := inputdef.Fields(cue.Optional(true))
if err != nil {
return rv, false, err
}
for iter.Next() {
lable, _ := iter.Value().Label()
lv := input.LookupPath(cue.MakePath(cue.Str(lable)))
if err != nil {
continue
}
if lv.Exists() {
re, isEqual, err := removeDefaultHelper(iter.Value(), lv)
if err == nil && !isEqual {
rv = rv.FillPath(cue.MakePath(cue.Str(lable)), re)
}
}
}
return rv, false, nil
case cue.ListKind:
val, _ := inputdef.Default()
err1 := input.Subsume(val)
err2 := val.Subsume(input)
if val.IsConcrete() && err1 == nil && err2 == nil {
return rv, true, nil
}
ele := inputdef.LookupPath(cue.MakePath(cue.AnyIndex))
fmt.Println("xxxxxxxxxxxxxxxxxxxxx ", ele.IncompleteKind())
if ele.IncompleteKind() == cue.BottomKind {
return rv, true, nil
}
iter, err := input.List()
if err != nil {
return rv, true, nil
}
index := 0
for iter.Next() {
re, isEqual, err := removeDefaultHelper(ele, iter.Value())
if err == nil && !isEqual {
rv = rv.FillPath(cue.MakePath(cue.Index(index)), re)
index++
}
}
// rv = rv.FillPath(cue.MakePath(cue.Str(lable)), rv)
return rv, false, nil
default:
val, _ := inputdef.Default()
err1 := input.Subsume(val)
err2 := val.Subsume(input)
if val.IsConcrete() && err1 == nil && err2 == nil {
return input, true, nil
}
return input, false, nil
}
}
// CUE returns the cue.Value representing the actual schema.
@ -164,7 +272,7 @@ var terminalMigrationFunc = func(x interface{}) (cue.Value, schema.VersionedCueS
// earlier schema) with a later schema.
func implicitMigration(v cue.Value, next schema.VersionedCueSchema) migrationFunc {
return func(x interface{}) (cue.Value, schema.VersionedCueSchema, error) {
w := v.Fill(x)
w := v.FillPath(cue.Path{}, x)
// TODO is it possible that migration would be successful, but there
// still exists some error here? Need to better understand internal CUE
// erroring rules? seems like incomplete cue.Value may always an Err()?

View File

@ -51,7 +51,7 @@ func TestDashboardValidity(t *testing.T) {
// TODO FIXME remove this once we actually have dashboard schema filled in
// enough that the tests pass, lol
t.Skip()
validdir := os.DirFS(filepath.Join("testdata", "artifacts", "dashboards"))
validdir := os.DirFS(filepath.Join("testdata", "artifacts", "dashboards", "basic"))
dash, err := BaseDashboardFamily(p)
require.NoError(t, err, "error while loading base dashboard scuemata")

View File

@ -30,7 +30,7 @@ func disjunctPanelScuemata(scuemap map[string]schema.VersionedCueSchema) (cue.Va
cv := mapPanelModel(id, sch)
mjv, miv := sch.Version()
parts = parts.Fill(cv, "allPanels", fmt.Sprintf("%s@%v.%v", id, mjv, miv))
parts = parts.FillPath(cue.MakePath(cue.Str("allPanels"), cue.Str(fmt.Sprintf("%s@%v.%v", id, mjv, miv))), cv)
sch = sch.Successor()
}
}
@ -63,7 +63,7 @@ func mapPanelModel(id string, vcs schema.VersionedCueSchema) cue.Value {
`, id, maj, min))
// TODO validate, especially with #PanelModel
return inter.Value().Fill(vcs.CUE(), "in", "model").LookupPath(cue.MakePath(cue.Str(("result"))))
return inter.Value().FillPath(cue.MakePath(cue.Str("in"), cue.Str("model")), vcs.CUE()).LookupPath(cue.MakePath(cue.Str(("result"))))
}
func readPanelModels(p BaseLoadPaths) (map[string]schema.VersionedCueSchema, error) {

View File

@ -0,0 +1,35 @@
Verifies common usecases for trimdefault/applydefault functions:
* Basic cases
* Real dashboard
-- CUE --
{
id?: number
uid: string
gnetId?: string
style: *"light" | "dark"
timezone?: *"browser" | "utc"
editable: bool | *true
schemaVersion: number | *25
version?: number
graphTooltip: >=0 & <=2 | *0
}
-- Full --
{
"id": 42,
"uid": "emal8gQMz",
"style": "light",
"editable": true,
"schemaVersion": 27,
"version": 2,
"graphTooltip": 0
}
-- Trimed --
{
"id": 42,
"uid": "emal8gQMz",
"schemaVersion": 27,
"version": 2
}

View File

@ -0,0 +1,41 @@
Verifies common usecases for trimdefault/applydefault functions:
* Basic cases
* Real dashboard
-- CUE --
{
timepicker?: {
collapse: bool | *false
enable: bool | *true
hidden: bool | *false
refresh_intervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
}
}
-- Full --
{
"timepicker": {
"collapse": true,
"enable":true,
"hidden":false,
"refresh_intervals":[
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
}
}
-- Trimed --
{
"timepicker": {
"collapse": true
}
}

View File

@ -0,0 +1,46 @@
Verifies common usecases for trimdefault/applydefault functions:
* Nested struct
* Simple array
-- CUE --
{
annotations?: list: [...{
builtIn: number | *0
datasource: string
enable?: bool | *true
hide?: bool | *false
iconColor?: string
name?: string
type: string | *"dashboard"
rawQuery?: string
showIn: number | *0
}]
}
-- Full --
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"name": "Annotations & Alerts",
"type": "dashboard",
"showIn": 0
}
]
}
}
-- Trimed --
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"name": "Annotations & Alerts"
}
]
}
}

View File

@ -0,0 +1,149 @@
package schemaloader
import (
"encoding/json"
"fmt"
"github.com/grafana/grafana"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/schema"
"github.com/grafana/grafana/pkg/schema/load"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/setting"
)
func init() {
registry.Register(&registry.Descriptor{
Name: ServiceName,
Instance: &SchemaLoaderService{},
})
}
const ServiceName = "SchemaLoader"
var baseLoadPath load.BaseLoadPaths = load.BaseLoadPaths{
BaseCueFS: grafana.CoreSchema,
DistPluginCueFS: grafana.PluginSchema,
}
type RenderUser struct {
OrgID int64
UserID int64
OrgRole string
}
type SchemaLoaderService struct {
log log.Logger
DashFamily schema.VersionedCueSchema
Cfg *setting.Cfg `inject:""`
}
func (rs *SchemaLoaderService) Init() error {
rs.log = log.New("schemaloader")
var err error
rs.DashFamily, err = load.BaseDashboardFamily(baseLoadPath)
if err != nil {
return fmt.Errorf("failed to load dashboard cue schema from path %q: %w", baseLoadPath, err)
}
return nil
}
func (rs *SchemaLoaderService) IsDisabled() bool {
if rs.Cfg == nil {
return true
}
return !rs.Cfg.IsTrimDefaultsEnabled()
}
func (rs *SchemaLoaderService) DashboardApplyDefaults(input *simplejson.Json) (*simplejson.Json, error) {
val, _ := input.Map()
val = removeNils(val)
data, _ := json.Marshal(val)
dsSchema := schema.Find(rs.DashFamily, schema.Latest())
result, err := dsSchema.ApplyDefaults(schema.Resource{Value: data})
if err != nil {
return input, err
}
output, err := simplejson.NewJson([]byte(result.Value.(string)))
if err != nil {
return input, err
}
return output, nil
}
func (rs *SchemaLoaderService) DashboardTrimDefaults(input simplejson.Json) (simplejson.Json, error) {
val, _ := input.Map()
val = removeNils(val)
data, _ := json.Marshal(val)
dsSchema, err := schema.SearchAndValidate(rs.DashFamily, data)
if err != nil {
return input, err
}
// spew.Dump(dsSchema)
result, err := dsSchema.TrimDefaults(schema.Resource{Value: data})
if err != nil {
return input, err
}
output, err := simplejson.NewJson([]byte(result.Value.(string)))
if err != nil {
return input, err
}
return *output, nil
}
func removeNils(initialMap map[string]interface{}) map[string]interface{} {
withoutNils := map[string]interface{}{}
for key, value := range initialMap {
_, ok := value.(map[string]interface{})
if ok {
value = removeNils(value.(map[string]interface{}))
withoutNils[key] = value
continue
}
_, ok = value.([]interface{})
if ok {
value = removeNilArray(value.([]interface{}))
withoutNils[key] = value
continue
}
if value != nil {
if val, ok := value.(string); ok {
if val == "" {
continue
}
}
withoutNils[key] = value
}
}
return withoutNils
}
func removeNilArray(initialArray []interface{}) []interface{} {
withoutNils := []interface{}{}
for _, value := range initialArray {
_, ok := value.(map[string]interface{})
if ok {
value = removeNils(value.(map[string]interface{}))
withoutNils = append(withoutNils, value)
continue
}
_, ok = value.([]interface{})
if ok {
value = removeNilArray(value.([]interface{}))
withoutNils = append(withoutNils, value)
continue
}
if value != nil {
if val, ok := value.(string); ok {
if val == "" {
continue
}
}
withoutNils = append(withoutNils, value)
}
}
return withoutNils
}

View File

@ -384,6 +384,11 @@ func (cfg Cfg) IsNgAlertEnabled() bool {
return cfg.FeatureToggles["ngalert"]
}
// IsTrimDefaultsEnabled returns whether the standalone trim dashboard default feature is enabled.
func (cfg Cfg) IsTrimDefaultsEnabled() bool {
return cfg.FeatureToggles["trimDefaults"]
}
// IsDatabaseMetricsEnabled returns whether the database instrumentation feature is enabled.
func (cfg Cfg) IsDatabaseMetricsEnabled() bool {
return cfg.FeatureToggles["database_metrics"]