mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
076e2ce06a
commit
748778fff0
@ -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
3
go.mod
@ -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
4
go.sum
@ -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=
|
||||
|
@ -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))
|
||||
|
@ -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")
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
116
pkg/schema/load/applydefault_test.go
Normal file
116
pkg/schema/load/applydefault_test.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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()?
|
||||
|
@ -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")
|
||||
|
@ -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) {
|
||||
|
35
pkg/schema/load/testdata/artifacts/dashboards/trimdefault/test1
vendored
Normal file
35
pkg/schema/load/testdata/artifacts/dashboards/trimdefault/test1
vendored
Normal 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
|
||||
}
|
41
pkg/schema/load/testdata/artifacts/dashboards/trimdefault/test2
vendored
Normal file
41
pkg/schema/load/testdata/artifacts/dashboards/trimdefault/test2
vendored
Normal 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
|
||||
}
|
||||
}
|
46
pkg/schema/load/testdata/artifacts/dashboards/trimdefault/test3
vendored
Normal file
46
pkg/schema/load/testdata/artifacts/dashboards/trimdefault/test3
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
149
pkg/services/schemaloader/schemaloader.go
Normal file
149
pkg/services/schemaloader/schemaloader.go
Normal 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(®istry.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
|
||||
}
|
@ -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"]
|
||||
|
Loading…
Reference in New Issue
Block a user