mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Perf: Disable core kind registry (#78568)
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
parent
b022ddeee8
commit
529271d7a8
@ -466,7 +466,6 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
})
|
||||
|
||||
dashboardRoute.Post("/calculate-diff", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.CalculateDashboardDiff))
|
||||
dashboardRoute.Post("/validate", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.ValidateDashboard))
|
||||
|
||||
dashboardRoute.Post("/db", authorize(ac.EvalAny(ac.EvalPermission(dashboards.ActionDashboardsCreate), ac.EvalPermission(dashboards.ActionDashboardsWrite))), routing.Wrap(hs.PostDashboard))
|
||||
dashboardRoute.Get("/home", routing.Wrap(hs.GetHomeDashboard))
|
||||
|
@ -17,7 +17,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/components/dashdiffs"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/kinds/dashboard"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
@ -792,72 +791,6 @@ func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.R
|
||||
return response.JSON(http.StatusOK, dashVersionMeta)
|
||||
}
|
||||
|
||||
// swagger:route POST /dashboards/validate dashboards alpha validateDashboard
|
||||
//
|
||||
// Validates a dashboard JSON against the schema.
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Responses:
|
||||
// 200: validateDashboardResponse
|
||||
// 412: validateDashboardResponse
|
||||
// 422: validateDashboardResponse
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
// 500: internalServerError
|
||||
func (hs *HTTPServer) ValidateDashboard(c *contextmodel.ReqContext) response.Response {
|
||||
cmd := dashboards.ValidateDashboardCommand{}
|
||||
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Bad request data", err)
|
||||
}
|
||||
|
||||
dk := hs.Kinds.Dashboard()
|
||||
dashboardBytes := []byte(cmd.Dashboard)
|
||||
|
||||
// POST api receives dashboard as a string of json (so line numbers for errors stay consistent),
|
||||
// but we need to parse the schema version out of it
|
||||
dashboardJson, err := simplejson.NewJson(dashboardBytes)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "unable to parse dashboard", err)
|
||||
}
|
||||
|
||||
schemaVersion, err := dashboardJson.Get("schemaVersion").Int()
|
||||
|
||||
isValid := false
|
||||
statusCode := http.StatusOK
|
||||
validationMessage := ""
|
||||
|
||||
// Only try to validate if the schemaVersion is at least the handoff version
|
||||
// (the minimum schemaVersion against which the dashboard schema is known to
|
||||
// work), or if schemaVersion is absent (which will happen once the Thema
|
||||
// schema becomes canonical).
|
||||
if err != nil || schemaVersion >= dashboard.HandoffSchemaVersion {
|
||||
// Schemas expect the dashboard to live in the spec field
|
||||
k8sResource := `{"spec": ` + cmd.Dashboard + "}"
|
||||
|
||||
_, _, validationErr := dk.JSONValueMux([]byte(k8sResource))
|
||||
|
||||
if validationErr == nil {
|
||||
isValid = true
|
||||
} else {
|
||||
validationMessage = validationErr.Error()
|
||||
statusCode = http.StatusUnprocessableEntity
|
||||
}
|
||||
} else {
|
||||
validationMessage = "invalid schema version"
|
||||
statusCode = http.StatusPreconditionFailed
|
||||
}
|
||||
|
||||
respData := &ValidateDashboardResponse{
|
||||
IsValid: isValid,
|
||||
Message: validationMessage,
|
||||
}
|
||||
|
||||
return response.JSON(statusCode, respData)
|
||||
}
|
||||
|
||||
// swagger:route POST /dashboards/calculate-diff dashboards calculateDashboardDiff
|
||||
//
|
||||
// Perform diff on two dashboards.
|
||||
@ -1303,9 +1236,3 @@ type DashboardVersionResponse struct {
|
||||
// in: body
|
||||
Body *dashver.DashboardVersionMeta `json:"body"`
|
||||
}
|
||||
|
||||
// swagger:response validateDashboardResponse
|
||||
type ValidateDashboardResponse struct {
|
||||
IsValid bool `json:"isValid"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||
"github.com/grafana/grafana/pkg/registry/corekind"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
@ -79,7 +78,6 @@ func TestGetHomeDashboard(t *testing.T) {
|
||||
SQLStore: dbtest.NewFakeDB(),
|
||||
preferenceService: prefService,
|
||||
dashboardVersionService: dashboardVersionService,
|
||||
Kinds: corekind.NewBase(nil),
|
||||
log: log.New("test-logger"),
|
||||
}
|
||||
|
||||
@ -515,60 +513,6 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Given a dashboard to validate", func(t *testing.T) {
|
||||
sqlmock := dbtest.NewFakeDB()
|
||||
|
||||
t.Run("When an invalid dashboard json is posted", func(t *testing.T) {
|
||||
cmd := dashboards.ValidateDashboardCommand{
|
||||
Dashboard: "{\"hello\": \"world\"}",
|
||||
}
|
||||
|
||||
role := org.RoleAdmin
|
||||
postValidateScenario(t, "When calling POST on", "/api/dashboards/validate", "/api/dashboards/validate", cmd, role, func(sc *scenarioContext) {
|
||||
callPostDashboard(sc)
|
||||
|
||||
result := sc.ToJSON()
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, sc.resp.Code)
|
||||
assert.False(t, result.Get("isValid").MustBool())
|
||||
assert.NotEmpty(t, result.Get("message").MustString())
|
||||
}, sqlmock)
|
||||
})
|
||||
|
||||
t.Run("When a dashboard with a too-low schema version is posted", func(t *testing.T) {
|
||||
cmd := dashboards.ValidateDashboardCommand{
|
||||
Dashboard: "{\"schemaVersion\": 1}",
|
||||
}
|
||||
|
||||
role := org.RoleAdmin
|
||||
postValidateScenario(t, "When calling POST on", "/api/dashboards/validate", "/api/dashboards/validate", cmd, role, func(sc *scenarioContext) {
|
||||
callPostDashboard(sc)
|
||||
|
||||
result := sc.ToJSON()
|
||||
assert.Equal(t, http.StatusPreconditionFailed, sc.resp.Code)
|
||||
assert.False(t, result.Get("isValid").MustBool())
|
||||
assert.Equal(t, "invalid schema version", result.Get("message").MustString())
|
||||
}, sqlmock)
|
||||
})
|
||||
|
||||
t.Run("When a valid dashboard is posted", func(t *testing.T) {
|
||||
devenvDashboard, readErr := os.ReadFile("../../devenv/dev-dashboards/home.json")
|
||||
assert.Empty(t, readErr)
|
||||
|
||||
cmd := dashboards.ValidateDashboardCommand{
|
||||
Dashboard: string(devenvDashboard),
|
||||
}
|
||||
|
||||
role := org.RoleAdmin
|
||||
postValidateScenario(t, "When calling POST on", "/api/dashboards/validate", "/api/dashboards/validate", cmd, role, func(sc *scenarioContext) {
|
||||
callPostDashboard(sc)
|
||||
|
||||
result := sc.ToJSON()
|
||||
assert.Equal(t, http.StatusOK, sc.resp.Code)
|
||||
assert.True(t, result.Get("isValid").MustBool())
|
||||
}, sqlmock)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Given two dashboards being compared", func(t *testing.T) {
|
||||
fakeDashboardVersionService := dashvertest.NewDashboardVersionServiceFake()
|
||||
fakeDashboardVersionService.ExpectedDashboardVersions = []*dashver.DashboardVersionDTO{
|
||||
@ -742,7 +686,6 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
AccessControl: accesscontrolmock.New(),
|
||||
DashboardService: dashboardService,
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
Kinds: corekind.NewBase(nil),
|
||||
starService: startest.NewStarServiceFake(),
|
||||
}
|
||||
hs.callGetDashboard(sc)
|
||||
@ -781,7 +724,6 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) {
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
DashboardService: dashboardService,
|
||||
dashboardVersionService: fakeDashboardVersionService,
|
||||
Kinds: corekind.NewBase(nil),
|
||||
QuotaService: quotatest.New(false, nil),
|
||||
userService: userSvc,
|
||||
CacheService: localcache.New(5*time.Minute, 10*time.Minute),
|
||||
@ -922,7 +864,6 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
|
||||
dashboardProvisioningService: dashboardProvisioningService,
|
||||
DashboardService: dashboardService,
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
Kinds: corekind.NewBase(nil),
|
||||
starService: startest.NewStarServiceFake(),
|
||||
}
|
||||
|
||||
@ -975,7 +916,6 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
|
||||
DashboardService: dashboardService,
|
||||
folderService: folderService,
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
Kinds: corekind.NewBase(nil),
|
||||
accesscontrolService: actest.FakeService{},
|
||||
log: log.New("test-logger"),
|
||||
}
|
||||
@ -996,42 +936,6 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
|
||||
})
|
||||
}
|
||||
|
||||
func postValidateScenario(t *testing.T, desc string, url string, routePattern string, cmd dashboards.ValidateDashboardCommand,
|
||||
role org.RoleType, fn scenarioFunc, sqlmock db.DB) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
hs := HTTPServer{
|
||||
Cfg: cfg,
|
||||
ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()),
|
||||
Live: newTestLive(t, db.InitTestDB(t)),
|
||||
QuotaService: quotatest.New(false, nil),
|
||||
LibraryPanelService: &mockLibraryPanelService{},
|
||||
LibraryElementService: &mockLibraryElementService{},
|
||||
SQLStore: sqlmock,
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
Kinds: corekind.NewBase(nil),
|
||||
}
|
||||
|
||||
sc := setupScenarioContext(t, url)
|
||||
sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
|
||||
c.Req.Body = mockRequestBody(cmd)
|
||||
c.Req.Header.Add("Content-Type", "application/json")
|
||||
sc.context = c
|
||||
sc.context.SignedInUser = &user.SignedInUser{
|
||||
OrgID: testOrgID,
|
||||
UserID: testUserID,
|
||||
}
|
||||
sc.context.OrgRole = role
|
||||
|
||||
return hs.ValidateDashboard(c)
|
||||
})
|
||||
|
||||
sc.m.Post(routePattern, sc.defaultHandler)
|
||||
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
func postDiffScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.CalculateDiffOptions,
|
||||
role org.RoleType, fn scenarioFunc, sqlmock db.DB, fakeDashboardVersionService *dashvertest.FakeDashboardVersionService) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
@ -1048,7 +952,6 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string
|
||||
SQLStore: sqlmock,
|
||||
dashboardVersionService: fakeDashboardVersionService,
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
Kinds: corekind.NewBase(nil),
|
||||
DashboardService: dashSvc,
|
||||
}
|
||||
|
||||
@ -1091,7 +994,6 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
|
||||
SQLStore: sqlStore,
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
dashboardVersionService: fakeDashboardVersionService,
|
||||
Kinds: corekind.NewBase(nil),
|
||||
accesscontrolService: actest.FakeService{},
|
||||
folderService: folderSvc,
|
||||
}
|
||||
|
@ -37,7 +37,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/middleware/requestmeta"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
"github.com/grafana/grafana/pkg/registry/corekind"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
@ -187,7 +186,6 @@ type HTTPServer struct {
|
||||
dashboardVersionService dashver.Service
|
||||
PublicDashboardsApi *publicdashboardsApi.Api
|
||||
starService star.Service
|
||||
Kinds *corekind.Base
|
||||
playlistService playlist.Service
|
||||
apiKeyService apikey.Service
|
||||
kvStore kvstore.KVStore
|
||||
@ -240,7 +238,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service,
|
||||
folderPermissionsService accesscontrol.FolderPermissionsService,
|
||||
dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service,
|
||||
starService star.Service, csrfService csrf.Service, basekinds *corekind.Base,
|
||||
starService star.Service, csrfService csrf.Service,
|
||||
playlistService playlist.Service, apiKeyService apikey.Service, kvStore kvstore.KVStore,
|
||||
secretsMigrator secrets.Migrator, secretsPluginManager plugins.SecretsPluginManager, secretsService secrets.Service,
|
||||
secretsPluginMigrator spm.SecretMigrationProvider, secretsStore secretsKV.SecretsKVStore,
|
||||
@ -329,7 +327,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
dashboardPermissionsService: dashboardPermissionsService,
|
||||
dashboardVersionService: dashboardVersionService,
|
||||
starService: starService,
|
||||
Kinds: basekinds,
|
||||
playlistService: playlistService,
|
||||
apiKeyService: apiKeyService,
|
||||
kvStore: kvStore,
|
||||
|
@ -6,6 +6,7 @@ package cuectx
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"cuelang.org/go/cue"
|
||||
"cuelang.org/go/cue/cuecontext"
|
||||
@ -26,14 +27,25 @@ var GoCoreKindParentPath = filepath.Join("pkg", "kinds")
|
||||
// contains one directory per kind, full of generated TS kind output: types and default consts.
|
||||
var TSCoreKindParentPath = filepath.Join("packages", "grafana-schema", "src", "raw")
|
||||
|
||||
var ctx = cuecontext.New()
|
||||
var rt = thema.NewRuntime(ctx)
|
||||
var (
|
||||
ctx *cue.Context
|
||||
rt *thema.Runtime
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func initContext() {
|
||||
once.Do(func() {
|
||||
ctx = cuecontext.New()
|
||||
rt = thema.NewRuntime(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
// GrafanaCUEContext returns Grafana's singleton instance of [cue.Context].
|
||||
//
|
||||
// All code within grafana/grafana that needs a *cue.Context should get it
|
||||
// from this function, when one was not otherwise provided.
|
||||
func GrafanaCUEContext() *cue.Context {
|
||||
initContext()
|
||||
return ctx
|
||||
}
|
||||
|
||||
@ -42,6 +54,7 @@ func GrafanaCUEContext() *cue.Context {
|
||||
// All code within grafana/grafana that needs a *thema.Runtime should get it
|
||||
// from this function, when one was not otherwise provided.
|
||||
func GrafanaThemaRuntime() *thema.Runtime {
|
||||
initContext()
|
||||
return rt
|
||||
}
|
||||
|
||||
@ -56,5 +69,5 @@ func GrafanaThemaRuntime() *thema.Runtime {
|
||||
// call it repeatedly. Most use cases should probably prefer making
|
||||
// their own Thema/CUE decoders.
|
||||
func JSONtoCUE(path string, b []byte) (cue.Value, error) {
|
||||
return vmux.NewJSONCodec(path).Decode(ctx, b)
|
||||
return vmux.NewJSONCodec(path).Decode(GrafanaCUEContext(), b)
|
||||
}
|
||||
|
@ -3,18 +3,12 @@ package corekind
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/grafana/kindsys"
|
||||
"github.com/grafana/thema"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cuectx"
|
||||
)
|
||||
|
||||
// KindSet contains all of the wire-style providers related to kinds.
|
||||
var KindSet = wire.NewSet(
|
||||
NewBase,
|
||||
)
|
||||
|
||||
var (
|
||||
baseOnce sync.Once
|
||||
defaultBase *Base
|
||||
|
@ -34,7 +34,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/middleware/csrf"
|
||||
"github.com/grafana/grafana/pkg/middleware/loggermw"
|
||||
apiregistry "github.com/grafana/grafana/pkg/registry/apis"
|
||||
"github.com/grafana/grafana/pkg/registry/corekind"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
||||
@ -314,7 +313,6 @@ var wireBasicSet = wire.NewSet(
|
||||
secretsStore.ProvideService,
|
||||
avatar.ProvideAvatarCacheServer,
|
||||
statscollector.ProvideService,
|
||||
corekind.KindSet,
|
||||
cuectx.GrafanaCUEContext,
|
||||
cuectx.GrafanaThemaRuntime,
|
||||
csrf.ProvideCSRFFilter,
|
||||
|
@ -260,15 +260,6 @@ type SaveDashboardCommand struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type ValidateDashboardCommand struct {
|
||||
Dashboard string `json:"dashboard" binding:"Required"`
|
||||
}
|
||||
|
||||
type TrimDashboardCommand struct {
|
||||
Dashboard *simplejson.Json `json:"dashboard" binding:"Required"`
|
||||
Meta *simplejson.Json `json:"meta"`
|
||||
}
|
||||
|
||||
type DashboardProvisioning struct {
|
||||
ID int64 `xorm:"pk autoincr 'id'"`
|
||||
DashboardID int64 `xorm:"dashboard_id"`
|
||||
|
@ -9158,17 +9158,6 @@
|
||||
"$ref": "#/definitions/UserProfileDTO"
|
||||
}
|
||||
},
|
||||
"validateDashboardResponse": {
|
||||
"description": "",
|
||||
"headers": {
|
||||
"isValid": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"viewPublicDashboardResponse": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
|
@ -22312,17 +22312,6 @@
|
||||
"$ref": "#/definitions/UserProfileDTO"
|
||||
}
|
||||
},
|
||||
"validateDashboardResponse": {
|
||||
"description": "(empty)",
|
||||
"headers": {
|
||||
"isValid": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"viewPublicDashboardResponse": {
|
||||
"description": "(empty)",
|
||||
"schema": {
|
||||
|
@ -516,16 +516,13 @@ export class BackendSrv implements BackendService {
|
||||
return this.get<DashboardDTO>(`/api/dashboards/uid/${uid}`);
|
||||
}
|
||||
|
||||
validateDashboard(dashboard: DashboardModel) {
|
||||
// We want to send the dashboard as a JSON string (in the JSON body payload) so we can get accurate error line numbers back
|
||||
const dashboardJson = JSON.stringify(dashboard, replaceJsonNulls, 2);
|
||||
|
||||
return this.request<ValidateDashboardResponse>({
|
||||
method: 'POST',
|
||||
url: `/api/dashboards/validate`,
|
||||
data: { dashboard: dashboardJson },
|
||||
showSuccessAlert: false,
|
||||
showErrorAlert: false,
|
||||
validateDashboard(dashboard: DashboardModel): Promise<ValidateDashboardResponse> {
|
||||
// support for this function will be implemented in the k8s flavored api-server
|
||||
// hidden by experimental feature flag:
|
||||
// config.featureToggles.showDashboardValidationWarnings
|
||||
return Promise.resolve({
|
||||
isValid: false,
|
||||
message: 'dashboard validation is supported',
|
||||
});
|
||||
}
|
||||
|
||||
@ -553,10 +550,3 @@ interface ValidateDashboardResponse {
|
||||
isValid: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
function replaceJsonNulls<T extends unknown>(key: string, value: T): T | undefined {
|
||||
if (typeof value === 'number' && !Number.isFinite(value)) {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
@ -1911,21 +1911,6 @@
|
||||
},
|
||||
"description": "(empty)"
|
||||
},
|
||||
"validateDashboardResponse": {
|
||||
"description": "(empty)",
|
||||
"headers": {
|
||||
"isValid": {
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"viewPublicDashboardResponse": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
|
Loading…
Reference in New Issue
Block a user