Transform DashboardScene into Schema V2 (#95546)

---------

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com>
Co-authored-by: Haris Rozajac <haris.rozajac12@gmail.com>
This commit is contained in:
Alexa V 2024-11-19 13:01:40 +01:00 committed by GitHub
parent 77a1b0ecce
commit 3c182a37fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 3955 additions and 30 deletions

View File

@ -2431,6 +2431,14 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "7"],
[0, 0, 0, "Do not use any type assertions.", "8"]
],
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
],
"public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
@ -2561,9 +2569,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not re-export imported variable (\`./VersionHistoryButtons\`)", "3"],
[0, 0, 0, "Do not re-export imported variable (\`./VersionHistoryComparison\`)", "4"]
],
"public/app/features/dashboard-scene/sharing/ShareExportTab.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],
"public/app/features/dashboard-scene/sharing/public-dashboards/ConfigPublicDashboard.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],

1
.github/CODEOWNERS vendored
View File

@ -672,6 +672,7 @@ playwright.config.ts @grafana/plugins-platform-frontend
# Kind definitions
/kinds/dashboard @grafana/dashboards-squad
/kinds/ @grafana/grafana-as-code
kindsv2/ @grafana/dashboards-squad
# Kind system and code generation
embed.go @grafana/grafana-as-code

View File

@ -77,6 +77,7 @@ RUN if [[ "$BINGO" = "true" ]]; then \
COPY embed.go Makefile build.go package.json ./
COPY cue.mod cue.mod
COPY kinds kinds
COPY kindsv2 kindsv2
COPY local local
COPY packages/grafana-schema packages/grafana-schema
COPY public/app/plugins public/app/plugins

View File

@ -146,6 +146,11 @@ gen-cue: ## Do all CUE/Thema code generation
go generate ./kinds/gen.go
go generate ./public/app/plugins/gen.go
.PHONY: gen-cuev2
gen-cuev2: ## Do all CUE code generation
@echo "generate code from .cue files (v2)"
go generate ./kindsv2/gen.go
.PHONY: gen-feature-toggles
gen-feature-toggles:
## First go test run fails because it will re-generate the feature toggles.

10
go.mod
View File

@ -16,7 +16,7 @@ require (
cloud.google.com/go/kms v1.18.5 // @grafana/grafana-backend-group
cloud.google.com/go/storage v1.43.0 // @grafana/grafana-backend-group
connectrpc.com/connect v1.17.0 // @grafana/observability-traces-and-profiling
cuelang.org/go v0.6.0-0.dev // @grafana/grafana-as-code
cuelang.org/go v0.8.2 // @grafana/grafana-as-code
filippo.io/age v1.1.1 // @grafana/identity-access-team
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // @grafana/partner-datasources
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 // @grafana/grafana-backend-group
@ -75,7 +75,7 @@ require (
github.com/grafana/alerting v0.0.0-20241021123319-be61d61f71e7 // @grafana/alerting-backend
github.com/grafana/authlib v0.0.0-20241024120339-84cd3a898e8a // @grafana/identity-access-team
github.com/grafana/authlib/claims v0.0.0-20241024115517-d30b00d7666d // @grafana/identity-access-team
github.com/grafana/codejen v0.0.3 // @grafana/dataviz-squad
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d // @grafana/dataviz-squad
github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
github.com/grafana/dataplane/sdata v0.0.9 // @grafana/observability-metrics
@ -109,7 +109,7 @@ require (
github.com/hashicorp/go-version v1.7.0 // @grafana/grafana-backend-group
github.com/hashicorp/golang-lru/v2 v2.0.7 // @grafana/alerting-backend
github.com/hashicorp/hcl/v2 v2.17.0 // @grafana/alerting-backend
github.com/huandu/xstrings v1.3.3 // @grafana/partner-datasources
github.com/huandu/xstrings v1.5.0 // @grafana/partner-datasources
github.com/influxdata/influxdb-client-go/v2 v2.13.0 // @grafana/observability-metrics
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // @grafana/grafana-app-platform-squad
github.com/jmespath/go-jmespath v0.4.0 // indirect; @grafana/grafana-backend-group
@ -266,7 +266,7 @@ require (
github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/emicklei/proto v1.10.0 // indirect
github.com/emicklei/proto v1.13.2 // indirect
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@ -382,7 +382,7 @@ require (
github.com/prometheus/common/sigv4 v0.1.0 // indirect
github.com/prometheus/exporter-toolkit v0.11.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 // indirect
github.com/protocolbuffers/txtpbfmt v0.0.0-20240116145035-ef3ab179eed6 // indirect
github.com/redis/rueidis v1.0.45 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect

15
go.sum
View File

@ -1833,8 +1833,8 @@ github.com/elazarl/goproxy/ext v0.0.0-20220115173737-adb46da277ac/go.mod h1:gNh8
github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emicklei/proto v1.10.0 h1:pDGyFRVV5RvV+nkBK9iy3q67FBy9Xa7vwrOTE+g5aGw=
github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY=
github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
@ -2266,8 +2266,8 @@ github.com/grafana/authlib v0.0.0-20241024120339-84cd3a898e8a h1:X3mroOOXdryRY6S
github.com/grafana/authlib v0.0.0-20241024120339-84cd3a898e8a/go.mod h1:XFhcSCEDeOgV43x41we7mRBcizDpsTScb7XOuYipQZg=
github.com/grafana/authlib/claims v0.0.0-20241024115517-d30b00d7666d h1:7nZfaXdC4Xc2ocMz5/Bx/3EsaEO34KsmA2RRcuogLnc=
github.com/grafana/authlib/claims v0.0.0-20241024115517-d30b00d7666d/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw=
github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk=
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ=
github.com/grafana/cue v0.0.0-20230926092038-971951014e3f/go.mod h1:okjJBHFQFer+a41sAe2SaGm1glWS8oEb6CmJvn5Zdws=
github.com/grafana/cuetsy v0.1.11 h1:I3IwBhF+UaQxRM79HnImtrAn8REGdb5M3+C4QrYHoWk=
@ -2470,8 +2470,9 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
@ -3034,8 +3035,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/prometheus/prometheus v0.52.0 h1:f7kHJgr7+zShpWdTCeKqbCWR7nKTScgLYQwRux9h1V0=
github.com/prometheus/prometheus v0.52.0/go.mod h1:3z74cVsmVH0iXOR5QBjB7Pa6A0KJeEAK5A6UsmAFb1g=
github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 h1:sadMIsgmHpEOGbUs6VtHBXRR1OHevnj7hLx9ZcdNGW4=
github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c=
github.com/protocolbuffers/txtpbfmt v0.0.0-20240116145035-ef3ab179eed6 h1:MAzmm+JtFxQwTPb1cVMLkemw2OxLy5AB/d/rxtAwGQQ=
github.com/protocolbuffers/txtpbfmt v0.0.0-20240116145035-ef3ab179eed6/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c=
github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU=
github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=

View File

@ -6,6 +6,7 @@ go 1.23.1
use (
. // skip:golangci-lint
./apps/playlist
./kindsv2
./pkg/aggregator
./pkg/apimachinery
./pkg/apiserver

52
kindsv2/gen.go Normal file
View File

@ -0,0 +1,52 @@
//go:generate go run gen.go
package main
import (
"context"
"os"
"cuelang.org/go/cue/cuecontext"
"github.com/grafana/cog"
)
type codegenTargets struct {
schemaPath string
outputPath string
}
func main() {
cueCtx := cuecontext.New()
targets := []codegenTargets{
{
schemaPath: "../packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue",
outputPath: "../packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.gen.ts",
},
}
for _, target := range targets {
rawSchema, err := os.ReadFile(target.schemaPath)
if err != nil {
panic(err)
}
value := cueCtx.CompileBytes(rawSchema)
if value.Err() != nil {
panic(value.Err())
}
codegenPipeline := cog.TypesFromSchema().
CUEValue("dashboard", value).
Typescript()
tsBytes, err := codegenPipeline.Run(context.Background())
if err != nil {
panic(err)
}
if err := os.WriteFile(target.outputPath, tsBytes, 0644); err != nil {
panic(err)
}
}
}

46
kindsv2/go.mod Normal file
View File

@ -0,0 +1,46 @@
module github.com/grafana/grafana/kindsv2
go 1.23.1
require (
cuelang.org/go v0.8.2
github.com/grafana/cog v0.0.0-20241029201114-f7f7db0c1070
)
require (
cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 // indirect
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/proto v1.13.2 // indirect
github.com/expr-lang/expr v1.16.9 // indirect
github.com/getkin/kin-openapi v0.128.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/invopop/yaml v0.3.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/protocolbuffers/txtpbfmt v0.0.0-20240116145035-ef3ab179eed6 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/yalue/merged_fs v1.3.0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/tools v0.26.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

94
kindsv2/go.sum Normal file
View File

@ -0,0 +1,94 @@
cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 h1:R5wwEcbEZSBmeyg91MJZTxfd7WpBo2jPof3AYjRbxwY=
cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565/go.mod h1:5A4xfTzHTXfeVJBU6RAUf+QrlfTCW+017q/QiW+sMLg=
cuelang.org/go v0.8.2 h1:vWfHI1kQlBvwkna7ktAqXjV5LUEAgU6vyMlJjvZZaDw=
cuelang.org/go v0.8.2/go.mod h1:CoDbYolfMms4BhWUlhD+t5ORnihR7wvjcfgyO9lL5FI=
github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg=
github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY=
github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI=
github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/getkin/kin-openapi v0.128.0 h1:jqq3D9vC9pPq1dGcOCv7yOp1DaEe7c/T1vzcLbITSp4=
github.com/getkin/kin-openapi v0.128.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk=
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
github.com/grafana/cog v0.0.0-20241029201114-f7f7db0c1070 h1:ncAkFmIq3UbPiPhQ2CuBzdTuwnDqLsFFNqEIXKaiPGA=
github.com/grafana/cog v0.0.0-20241029201114-f7f7db0c1070/go.mod h1:FqZi9WZ/Uzvs3tvo7l+OViThCpfvu3KDGDqHCB2LNbg=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/protocolbuffers/txtpbfmt v0.0.0-20240116145035-ef3ab179eed6 h1:MAzmm+JtFxQwTPb1cVMLkemw2OxLy5AB/d/rxtAwGQQ=
github.com/protocolbuffers/txtpbfmt v0.0.0-20240116145035-ef3ab179eed6/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY=
github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,785 @@
package dashboard
DashboardV2Spec: {
// Unique numeric identifier for the dashboard.
// `id` is internal to a specific Grafana instance. `uid` should be used to identify a dashboard across Grafana instances.
id?: int64
// Title of dashboard.
title: string
// Description of dashboard.
description?: string
// Configuration of dashboard cursor sync behavior.
// "Off" for no shared crosshair or tooltip (default).
// "Crosshair" for shared crosshair.
// "Tooltip" for shared crosshair AND shared tooltip.
cursorSync: DashboardCursorSync
// When set to true, the dashboard will redraw panels at an interval matching the pixel width.
// This will keep data "moving left" regardless of the query refresh rate. This setting helps
// avoid dashboards presenting stale live data.
liveNow?: bool
// When set to true, the dashboard will load all panels in the dashboard when it's loaded.
preload: bool
// Whether a dashboard is editable or not.
editable?: bool | *true
// Links with references to other dashboards or external websites.
links: [...DashboardLink]
// Tags associated with dashboard.
tags?: [...string]
timeSettings: TimeSettingsSpec
// Configured template variables.
variables: [...QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind]
elements: [ElementReference.name]: PanelKind // |* more element types in the future
annotations: [...AnnotationQueryKind]
layout: GridLayoutKind
// version: will rely on k8s resource versioning, via metadata.resorceVersion
// revision?: int // for plugins only
// gnetId?: string // ??? Wat is this used for?
}
AnnotationPanelFilter: {
// Should the specified panels be included or excluded
exclude?: bool | *false
// Panel IDs that should be included or excluded
ids: [...uint8]
}
// "Off" for no shared crosshair or tooltip (default).
// "Crosshair" for shared crosshair.
// "Tooltip" for shared crosshair AND shared tooltip.
DashboardCursorSync: "Off" | "Crosshair" | "Tooltip"
// Links with references to other dashboards or external resources
DashboardLink: {
// Title to display with the link
title: string
// Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)
// FIXME: The type is generated as `type: DashboardLinkType | dashboardLinkType.Link;` but it should be `type: DashboardLinkType`
type: DashboardLinkType
// Icon name to be displayed with the link
icon: string
// Tooltip to display when the user hovers their mouse over it
tooltip: string
// Link URL. Only required/valid if the type is link
url?: string
// List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards
tags: [...string]
// If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards
asDropdown: bool | *false
// If true, the link will be opened in a new tab
targetBlank: bool | *false
// If true, includes current template variables values in the link as query params
includeVars: bool | *false
// If true, includes current time range in the link as query params
keepTime: bool | *false
}
DataSourceRef: {
// The plugin type-id
type?: string
// Specific datasource instance
uid?: string
}
// Transformations allow to manipulate data returned by a query before the system applies a visualization.
// Using transformations you can: rename fields, join time series data, perform mathematical operations across queries,
// use the output of one transformation as the input to another transformation, etc.
DataTransformerConfig: {
// Unique identifier of transformer
id: string
// Disabled transformations are skipped
disabled?: bool
// Optional frame matcher. When missing it will be applied to all results
filter?: MatcherConfig
// Where to pull DataFrames from as input to transformation
topic?: "series" | "annotations" | "alertStates" // replaced with common.DataTopic
// Options to be passed to the transformer
// Valid options depend on the transformer id
options: _
}
// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.
// Each column within this structure is called a field. A field can represent a single time series or table column.
// Field options allow you to change how the data is displayed in your visualizations.
FieldConfigSource: {
// Defaults are the options applied to all fields.
defaults: FieldConfig
// Overrides are the options applied to specific fields overriding the defaults.
overrides: [...{
matcher: MatcherConfig
properties: [...DynamicConfigValue]
}]
}
// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.
// Each column within this structure is called a field. A field can represent a single time series or table column.
// Field options allow you to change how the data is displayed in your visualizations.
FieldConfig: {
// The display value for this field. This supports template variables blank is auto
displayName?: string
// This can be used by data sources that return and explicit naming structure for values and labels
// When this property is configured, this value is used rather than the default naming strategy.
displayNameFromDS?: string
// Human readable field metadata
description?: string
// An explicit path to the field in the datasource. When the frame meta includes a path,
// This will default to `${frame.meta.path}/${field.name}
//
// When defined, this value can be used as an identifier within the datasource scope, and
// may be used to update the results
path?: string
// True if data source can write a value to the path. Auth/authz are supported separately
writeable?: bool
// True if data source field supports ad-hoc filters
filterable?: bool
// Unit a field should use. The unit you select is applied to all fields except time.
// You can use the units ID availables in Grafana or a custom unit.
// Available units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts
// As custom unit, you can use the following formats:
// `suffix:<suffix>` for custom unit that should go after value.
// `prefix:<prefix>` for custom unit that should go before value.
// `time:<format>` For custom date time formats type for example `time:YYYY-MM-DD`.
// `si:<base scale><unit characters>` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.
// `count:<unit>` for a custom count unit.
// `currency:<unit>` for custom a currency unit.
unit?: string
// Specify the number of decimals Grafana includes in the rendered value.
// If you leave this field blank, Grafana automatically truncates the number of decimals based on the value.
// For example 1.1234 will display as 1.12 and 100.456 will display as 100.
// To display all decimals, set the unit to `String`.
decimals?: number
// The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.
min?: number
// The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.
max?: number
// Convert input values into a display string
mappings?: [...ValueMapping]
// Map numeric values to states
thresholds?: ThresholdsConfig
// Panel color configuration
color?: FieldColor
// The behavior when clicking on a result
links?: [...]
// Alternative to empty string
noValue?: string
// custom is specified by the FieldConfig field
// in panel plugin schemas.
custom?: {...}
}
DynamicConfigValue: {
id: string | *""
value?: _
}
// Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.
// It comes with in id ( to resolve implementation from registry) and a configuration thats specific to a particular matcher type.
MatcherConfig: {
// The matcher id. This is used to find the matcher implementation from registry.
id: string | *""
// The matcher options. This is specific to the matcher implementation.
options?: _
}
Threshold: {
value: number | null
color: string
}
ThresholdsMode: "absolute" | "percentage"
ThresholdsConfig: {
mode: ThresholdsMode
steps: [...Threshold]
}
ValueMapping: ValueMap | RangeMap | RegexMap | SpecialValueMap
// Supported value mapping types
// `value`: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.
// `range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.
// `regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.
// `special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.
MappingType: "value" | "range" | "regex" | "special"
// Maps text values to a color or different display text and color.
// For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.
ValueMap: {
type: MappingType & "value"
// Map with <value_to_match>: ValueMappingResult. For example: { "10": { text: "Perfection!", color: "green" } }
options: [string]: ValueMappingResult
}
// Maps numerical ranges to a display text and color.
// For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.
RangeMap: {
type: MappingType & "range"
// Range to match against and the result to apply when the value is within the range
options: {
// Min value of the range. It can be null which means -Infinity
from: float64 | null
// Max value of the range. It can be null which means +Infinity
to: float64 | null
// Config to apply when the value is within the range
result: ValueMappingResult
}
}
// Maps regular expressions to replacement text and a color.
// For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.
RegexMap: {
type: MappingType & "regex"
// Regular expression to match against and the result to apply when the value matches the regex
options: {
// Regular expression to match against
pattern: string
// Config to apply when the value matches the regex
result: ValueMappingResult
}
}
// Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.
// See SpecialValueMatch to see the list of special values.
// For example, you can configure a special value mapping so that null values appear as N/A.
SpecialValueMap: {
type: MappingType & "special"
options: {
// Special value to match against
match: SpecialValueMatch
// Config to apply when the value matches the special value
result: ValueMappingResult
}
}
// Special value types supported by the `SpecialValueMap`
SpecialValueMatch: "true" | "false" | "null" | "nan" | "null+nan" | "empty"
// Result used as replacement with text and color when the value matches
ValueMappingResult: {
// Text to display when the value matches
text?: string
// Text to use when the value matches
color?: string
// Icon to display when the value matches. Only specific visualizations.
icon?: string
// Position in the mapping array. Only used internally.
index?: int32
}
// Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.
// Continuous color interpolates a color using the percentage of a value relative to min and max.
// Accepted values are:
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
// `continuous-YlRd`: Continuous Yellow-Red palette mode
// `continuous-BlPu`: Continuous Blue-Purple palette mode
// `continuous-YlBl`: Continuous Yellow-Blue palette mode
// `continuous-blues`: Continuous Blue palette mode
// `continuous-reds`: Continuous Red palette mode
// `continuous-greens`: Continuous Green palette mode
// `continuous-purples`: Continuous Purple palette mode
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
FieldColorSeriesByMode: "min" | "max" | "last"
// Map a field to a color.
FieldColor: {
// The main color scheme mode.
mode: FieldColorModeId
// The fixed color value for fixed or shades color modes.
fixedColor?: string
// Some visualizations need to know how to assign a series color from by value color schemes.
seriesBy?: FieldColorSeriesByMode
}
// Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)
DashboardLinkType: "link" | "dashboards"
// --- Common types ---
Kind: {
kind: string,
spec: _
metadata?: _
}
// --- Kinds ---
VizConfigSpec: {
pluginVersion: string
options: [string]: _
fieldConfig: FieldConfigSource
}
VizConfigKind: {
// The kind of a VizConfigKind is the plugin ID
kind: string
spec: VizConfigSpec
}
AnnotationQuerySpec: {
datasource: DataSourceRef
query: DataQueryKind
// TODO: Should be figured out based on datasource (Grafana ds)
// builtIn?: int
// Below are currently existing options for annotation queries
enable: bool
filter: AnnotationPanelFilter
hide: bool
iconColor: string
name: string
}
AnnotationQueryKind: {
kind: "AnnotationQuery"
spec: AnnotationQuerySpec
}
QueryOptionsSpec: {
timeFrom?: string
maxDataPoints?: int
timeShift?: string
queryCachingTTL?: int
interval?: string
cacheTimeout?: string
}
DataQueryKind: {
// The kind of a DataQueryKind is the datasource type
kind: string
spec: [string]: _
}
PanelQuerySpec: {
query: DataQueryKind
datasource: DataSourceRef
refId: string
hidden: bool
}
PanelQueryKind: {
kind: "PanelQuery"
spec: PanelQuerySpec
}
TransformationKind: {
// The kind of a TransformationKind is the transformation ID
kind: string
spec: DataTransformerConfig
}
QueryGroupSpec: {
queries: [...PanelQueryKind]
transformations: [...TransformationKind]
queryOptions: QueryOptionsSpec
}
QueryGroupKind: {
kind: "QueryGroup"
spec: QueryGroupSpec
}
// Time configuration
// It defines the default time config for the time picker, the refresh picker for the specific dashboard.
TimeSettingsSpec: {
// Timezone of dashboard. Accepted values are IANA TZDB zone ID or "browser" or "utc".
timezone?: string | *"browser"
// Start time range for dashboard.
// Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z".
from: string | *"now-6h"
// End time range for dashboard.
// Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z".
to: string | *"now"
// Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d".
autoRefresh: string // v1: refresh
// Interval options available in the refresh picker dropdown.
autoRefreshIntervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] // v1: timepicker.refresh_intervals
// Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.
quickRanges: [...string] | *["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] // v1: timepicker.time_options , not exposed in the UI
// Whether timepicker is visible or not.
hideTimepicker: bool // v1: timepicker.hidden
// Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday".
weekStart: string
// The month that the fiscal year starts on. 0 = January, 11 = December
fiscalYearStartMonth: int
// Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.
nowDelay?: string // v1: timepicker.nowDelay
}
GridLayoutItemSpec: {
x: int
y: int
width: int
height: int
element: ElementReference // reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference
}
GridLayoutItemKind: {
kind: "GridLayoutItem"
spec: GridLayoutItemSpec
}
GridLayoutSpec: {
items: [...GridLayoutItemKind]
}
GridLayoutKind: {
kind: "GridLayout"
spec: GridLayoutSpec
}
PanelSpec: {
uid: string
title: string
description: string
links: [...DashboardLink]
data: QueryGroupKind
vizConfig: VizConfigKind
}
PanelKind: {
kind: "Panel"
spec: PanelSpec
}
ElementReference: {
kind: "ElementReference"
name: string
}
// Start FIXME: variables - in CUE PR - this are things that should be added into the cue schema
// TODO: properties such as `hide`, `skipUrlSync`, `multi` are type boolean, and in the old schema they are conditional,
// should we make them conditional in the new schema as well? or should we make them required but default to false?
// Variable types
VariableValue: VariableValueSingle | [...VariableValueSingle]
VariableValueSingle: string | bool | number | CustomVariableValue
// Custom formatter variable
CustomFormatterVariable: {
name: string
type: VariableType
multi: bool
includeAll: bool
}
// Custom variable value
CustomVariableValue: {
// The format name or function used in the expression
formatter: *null | string | VariableCustomFormatterFn
}
// Custom formatter function
VariableCustomFormatterFn: {
value: _
legacyVariableModel: {
name: string
type: VariableType
multi: bool
includeAll: bool
}
legacyDefaultFormatter?: VariableCustomFormatterFn
}
// Dashboard variable type
// `query`: Query-generated list of values such as metric names, server names, sensor IDs, data centers, and so on.
// `adhoc`: Key/value filters that are automatically added to all metric queries for a data source (Prometheus, Loki, InfluxDB, and Elasticsearch only).
// `constant`: Define a hidden constant.
// `datasource`: Quickly change the data source for an entire dashboard.
// `interval`: Interval variables represent time spans.
// `textbox`: Display a free text input field with an optional default value.
// `custom`: Define the variable options manually using a comma-separated list.
// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
"system" | "snapshot"
// Sort variable options
// Accepted values are:
// `disabled`: No sorting
// `alphabeticalAsc`: Alphabetical ASC
// `alphabeticalDesc`: Alphabetical DESC
// `numericalAsc`: Numerical ASC
// `numericalDesc`: Numerical DESC
// `alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC
// `alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC
// `naturalAsc`: Natural ASC
// `naturalDesc`: Natural DESC
// VariableSort enum with default value
VariableSort: "disabled" | "alphabeticalAsc" | "alphabeticalDesc" | "numericalAsc" | "numericalDesc" | "alphabeticalCaseInsensitiveAsc" | "alphabeticalCaseInsensitiveDesc" | "naturalAsc" | "naturalDesc"
// Options to config when to refresh a variable
// `never`: Never refresh the variable
// `onDashboardLoad`: Queries the data source every time the dashboard loads.
// `onTimeRangeChanged`: Queries the data source when the dashboard time range changes.
VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged"
// Determine if the variable shows on dashboard
// Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing).
VariableHide: *"dontHide" | "hideLabel" | "hideVariable"
// FIXME: should we introduce this? --- Variable value option
VariableValueOption: {
label: string
value: VariableValueSingle
group?: string
}
// Variable option specification
VariableOption: {
// Whether the option is selected or not
selected?: bool
// Text to be displayed for the option
text: string | [...string]
// Value of the option
value: string | [...string]
}
// Query variable specification
QueryVariableSpec: {
name: string | *""
current: VariableOption | *{
text: ""
value: ""
}
label?: string
hide: VariableHide
refresh: VariableRefresh
skipUrlSync: bool | *false
description?: string
datasource: DataSourceRef | *{}
query: string | DataQueryKind | *""
regex: string | *""
sort: VariableSort
definition?: string
options: [...VariableOption] | *[]
multi: bool | *false
includeAll: bool | *false
allValue?: string
placeholder?: string
}
// Query variable kind
QueryVariableKind: {
kind: "QueryVariable"
spec: QueryVariableSpec
}
// Text variable specification
TextVariableSpec: {
name: string | *""
current: VariableOption | *{
text: ""
value: ""
}
query: string | *""
label?: string
hide: VariableHide
skipUrlSync: bool | *false
description?: string
}
// Text variable kind
TextVariableKind: {
kind: "TextVariable"
spec: TextVariableSpec
}
// Constant variable specification
ConstantVariableSpec: {
name: string | *""
query: string | *""
current: VariableOption | *{
text: ""
value: ""
}
label?: string
hide: VariableHide
skipUrlSync: bool | *false
description?: string
}
// Constant variable kind
ConstantVariableKind: {
kind: "ConstantVariable"
spec: ConstantVariableSpec
}
// Datasource variable specification
DatasourceVariableSpec: {
name: string | *""
pluginId: string | *""
refresh: VariableRefresh
regex: string | *""
current: VariableOption | *{
text: ""
value: ""
}
defaultOptionEnabled: bool | *false
options: [...VariableOption] | *[]
multi: bool | *false
includeAll: bool | *false
allValue?: string
label?: string
hide: VariableHide
skipUrlSync: bool | *false
description?: string
}
// Datasource variable kind
DatasourceVariableKind: {
kind: "DatasourceVariable"
spec: DatasourceVariableSpec
}
// Interval variable specification
IntervalVariableSpec: {
name: string | *""
query: string | *""
current: VariableOption | *{
text: ""
value: ""
}
options: [...VariableOption] | *[]
auto: bool | *false
auto_min: string | *""
auto_count: int | *0
refresh: VariableRefresh
label?: string
hide: VariableHide
skipUrlSync: bool | *false
description?: string
}
// Interval variable kind
IntervalVariableKind: {
kind: "IntervalVariable"
spec: IntervalVariableSpec
}
// Custom variable specification
CustomVariableSpec: {
name: string | *""
query: string | *""
current: VariableOption
options: [...VariableOption] | *[]
multi: bool | *false
includeAll: bool | *false
allValue?: string
label?: string
hide: VariableHide
skipUrlSync: bool | *false
description?: string
}
// Custom variable kind
CustomVariableKind: {
kind: "CustomVariable"
spec: CustomVariableSpec
}
// GroupBy variable specification
GroupByVariableSpec: {
name: string | *""
datasource: DataSourceRef | *{}
current: VariableOption | *{
text: ""
value: ""
}
options: [...VariableOption] | *[]
multi: bool | *false
includeAll: bool | *false
allValue?: string
label?: string
hide: VariableHide
skipUrlSync: bool | *false
description?: string
}
// Group variable kind
GroupByVariableKind: {
kind: "GroupByVariable"
spec: GroupByVariableSpec
}
// Adhoc variable specification
AdhocVariableSpec: {
name: string | *""
datasource: DataSourceRef | *{}
baseFilters: [...AdHocFilterWithLabels] | *[]
filters: [...AdHocFilterWithLabels] | *[]
defaultKeys: [...MetricFindValue] | *[]
label?: string
hide: VariableHide
skipUrlSync: bool | *false
description?: string
}
// Define the MetricFindValue type
MetricFindValue: {
text: string
value?: string | number
group?: string
expandable?: bool
}
// Define the AdHocFilterWithLabels type
AdHocFilterWithLabels: {
key: string,
operator: string,
value: string,
values?: [...string],
keyLabel?: string,
valueLabels?: [...string],
forceEdit?: bool,
// @deprecated
condition?: string,
}
// Adhoc variable kind
AdhocVariableKind: {
kind: "AdhocVariable"
spec: AdhocVariableSpec
}

View File

@ -0,0 +1,149 @@
import { DashboardCursorSync, DashboardV2Spec } from './dashboard.gen';
export const handyTestingSchema: DashboardV2Spec = {
id: 1,
title: 'Default Dashboard',
description: 'This is a default dashboard',
cursorSync: DashboardCursorSync.Off,
liveNow: false,
preload: false,
editable: true,
links: [],
tags: [],
timeSettings: {
timezone: 'browser',
from: 'now-6h',
to: 'now',
autoRefresh: '10s',
autoRefreshIntervals: ['10s', '1m', '5m', '15m', '30m', '1h', '6h', '12h', '1d'],
quickRanges: ['now/d', 'now/w', 'now/M', 'now/y'],
hideTimepicker: false,
weekStart: 'sunday',
fiscalYearStartMonth: 1,
},
elements: {
timeSeriesTest: {
kind: 'Panel',
spec: {
title: 'Time Series Test',
description: 'This is a test panel',
uid: 'timeSeriesTest',
links: [],
data: {
kind: 'QueryGroup',
spec: {
queries: [
{
kind: 'PanelQuery',
spec: {
query: {
kind: 'prometheus',
spec: {
query: 'up',
},
},
datasource: { uid: 'gdev-prometheus', type: 'prometheus' },
hidden: false,
refId: 'A',
},
},
],
transformations: [
{
kind: 'limit',
spec: {
id: 'limit', // id is competing w/ kind
options: {
limit: 10,
},
},
},
],
queryOptions: {
maxDataPoints: 100,
cacheTimeout: '1m',
},
},
},
vizConfig: {
kind: 'timeseries',
spec: {
pluginVersion: '11.0.0',
options: {},
fieldConfig: {
defaults: {},
overrides: [],
},
},
},
},
},
},
layout: {
kind: 'GridLayout',
spec: {
items: [
{
kind: 'GridLayoutItem',
spec: {
element: { kind: 'ElementReference', name: 'timeSeriesTest' },
x: 0,
y: 0,
width: 12,
height: 6,
},
},
],
},
},
variables: [],
annotations: [
{
kind: 'AnnotationQuery',
spec: {
datasource: { type: 'datasource', uid: 'grafana' },
query: {
kind: 'grafana',
spec: {
queryType: 'timeRegions',
matchAny: false,
timeRegion: {
from: '12:27',
fromDayOfWeek: 2,
timezone: 'browser',
to: '11:30',
toDayOfWeek: 2,
},
},
},
enable: true,
filter: {
ids: [],
},
hide: false,
iconColor: 'blue',
name: 'Grafana annotations',
},
},
{
kind: 'AnnotationQuery',
spec: {
datasource: { uid: 'gdev-prometheus', type: 'prometheus' },
query: {
kind: 'prometheus',
spec: {
query: 'up',
},
},
enable: true,
filter: {
ids: [],
},
hide: false,
iconColor: 'red',
name: 'Prometheus annotations',
},
},
],
};

View File

@ -25,7 +25,7 @@ import {
} from '@grafana/scenes';
import { DataSourceRef, VariableRefresh } from '@grafana/schema';
import { sceneVariablesSetToVariables } from './sceneVariablesSetToVariables';
import { sceneVariablesSetToSchemaV2Variables, sceneVariablesSetToVariables } from './sceneVariablesSetToVariables';
const runRequestMock = jest.fn().mockReturnValue(
of<PanelData>({
@ -713,4 +713,555 @@ describe('sceneVariablesSetToVariables', () => {
expect(() => sceneVariablesSetToVariables(set)).toThrow('Unsupported variable type');
});
});
describe('sceneVariablesSetToSchemaV2Variables', () => {
it('should handle QueryVariable', () => {
const variable = new QueryVariable({
name: 'test',
label: 'test-label',
description: 'test-desc',
value: ['selected-value'],
text: ['selected-value-text'],
datasource: { uid: 'fake-std', type: 'fake-std' },
query: 'query',
includeAll: true,
allValue: 'test-all',
isMulti: true,
});
const set = new SceneVariableSet({
variables: [variable],
});
const result = sceneVariablesSetToSchemaV2Variables(set);
expect(result).toHaveLength(1);
expect(result[0]).toMatchInlineSnapshot(`
{
"kind": "QueryVariable",
"spec": {
"allValue": "test-all",
"current": {
"text": [
"selected-value-text",
],
"value": [
"selected-value",
],
},
"datasource": {
"type": "fake-std",
"uid": "fake-std",
},
"definition": undefined,
"description": "test-desc",
"hide": "dontHide",
"includeAll": true,
"label": "test-label",
"multi": true,
"name": "test",
"options": [],
"query": "query",
"refresh": "onDashboardLoad",
"regex": "",
"skipUrlSync": false,
"sort": "disabled",
},
}
`);
});
it('should handle CustomVariable', () => {
const variable = new CustomVariable({
name: 'test',
label: 'test-label',
description: 'test-desc',
value: ['test', 'test2'],
text: ['test', 'test2'],
query: 'test,test1,test2',
options: [
{ label: 'test', value: 'test' },
{ label: 'test1', value: 'test1' },
{ label: 'test2', value: 'test2' },
],
includeAll: true,
allValue: 'test-all',
isMulti: true,
});
const set = new SceneVariableSet({
variables: [variable],
});
const result = sceneVariablesSetToSchemaV2Variables(set);
expect(result).toHaveLength(1);
expect(result[0]).toMatchInlineSnapshot(`
{
"kind": "CustomVariable",
"spec": {
"allValue": "test-all",
"current": {
"text": [
"test",
"test2",
],
"value": [
"test",
"test2",
],
},
"description": "test-desc",
"hide": "dontHide",
"includeAll": true,
"label": "test-label",
"multi": true,
"name": "test",
"options": [
{
"selected": true,
"text": "test",
"value": "test",
},
{
"selected": false,
"text": "test1",
"value": "test1",
},
{
"selected": true,
"text": "test2",
"value": "test2",
},
],
"query": "test,test1,test2",
"skipUrlSync": false,
},
}
`);
});
it('should handle DatasourceVariable', () => {
const variable = new DataSourceVariable({
name: 'test',
label: 'test-label',
description: 'test-desc',
value: ['selected-ds-1', 'selected-ds-2'],
text: ['selected-ds-1-text', 'selected-ds-2-text'],
pluginId: 'fake-std',
includeAll: true,
allValue: 'test-all',
isMulti: true,
});
const set = new SceneVariableSet({
variables: [variable],
});
const result = sceneVariablesSetToSchemaV2Variables(set);
expect(result).toHaveLength(1);
expect(result[0]).toMatchInlineSnapshot(`
{
"kind": "DatasourceVariable",
"spec": {
"allValue": "test-all",
"current": {
"text": [
"selected-ds-1-text",
"selected-ds-2-text",
],
"value": [
"selected-ds-1",
"selected-ds-2",
],
},
"defaultOptionEnabled": false,
"description": "test-desc",
"hide": "dontHide",
"includeAll": true,
"label": "test-label",
"multi": true,
"name": "test",
"options": [],
"pluginId": "fake-std",
"refresh": "onDashboardLoad",
"regex": "",
"skipUrlSync": false,
},
}
`);
});
it('should handle ConstantVariable', () => {
const variable = new ConstantVariable({
name: 'test',
label: 'test-label',
description: 'test-desc',
value: 'constant value',
skipUrlSync: true,
});
const set = new SceneVariableSet({
variables: [variable],
});
const result = sceneVariablesSetToSchemaV2Variables(set);
expect(result).toHaveLength(1);
expect(result[0]).toMatchInlineSnapshot(`
{
"kind": "ConstantVariable",
"spec": {
"current": {
"text": undefined,
"value": "constant value",
},
"description": "test-desc",
"hide": "dontHide",
"label": "test-label",
"name": "test",
"query": "constant value",
"skipUrlSync": true,
},
}
`);
});
it('should handle TextBoxVariable', () => {
const variable = new TextBoxVariable({
name: 'test',
label: 'test-label',
description: 'test-desc',
value: 'text value',
skipUrlSync: true,
});
const set = new SceneVariableSet({
variables: [variable],
});
const result = sceneVariablesSetToSchemaV2Variables(set);
expect(result).toHaveLength(1);
expect(result[0]).toMatchInlineSnapshot(`
{
"kind": "TextVariable",
"spec": {
"current": {
"text": "text value",
"value": "text value",
},
"description": "test-desc",
"hide": "dontHide",
"label": "test-label",
"name": "test",
"query": "text value",
"skipUrlSync": true,
},
}
`);
});
it('should handle IntervalVariable', () => {
const variable = new IntervalVariable({
intervals: ['1m', '2m', '3m', '1h', '1d'],
value: '1m',
refresh: VariableRefresh.onDashboardLoad,
});
const set = new SceneVariableSet({
variables: [variable],
});
const result = sceneVariablesSetToSchemaV2Variables(set);
expect(result[0]).toMatchInlineSnapshot(`
{
"kind": "IntervalVariable",
"spec": {
"auto": false,
"auto_count": 30,
"auto_min": "10s",
"current": {
"text": undefined,
"value": "1m",
},
"description": undefined,
"hide": "dontHide",
"label": undefined,
"name": "",
"options": [
{
"selected": true,
"text": "1m",
"value": "1m",
},
{
"selected": false,
"text": "2m",
"value": "2m",
},
{
"selected": false,
"text": "3m",
"value": "3m",
},
{
"selected": false,
"text": "1h",
"value": "1h",
},
{
"selected": false,
"text": "1d",
"value": "1d",
},
],
"query": "1m,2m,3m,1h,1d",
"refresh": "onTimeRangeChanged",
"skipUrlSync": false,
},
}
`);
});
it('should handle AdHocFiltersVariable', () => {
const variable = new AdHocFiltersVariable({
name: 'test',
label: 'test-label',
description: 'test-desc',
datasource: { uid: 'fake-std', type: 'fake-std' },
filters: [
{
key: 'filterTest',
operator: '=',
value: 'test',
},
],
baseFilters: [
{
key: 'baseFilterTest',
operator: '=',
value: 'test',
},
],
});
const set = new SceneVariableSet({
variables: [variable],
});
const result = sceneVariablesSetToSchemaV2Variables(set);
expect(result).toHaveLength(1);
expect(result[0]).toMatchInlineSnapshot(`
{
"kind": "AdhocVariable",
"spec": {
"baseFilters": [
{
"key": "baseFilterTest",
"operator": "=",
"value": "test",
},
],
"datasource": {
"type": "fake-std",
"uid": "fake-std",
},
"defaultKeys": [],
"description": "test-desc",
"filters": [
{
"key": "filterTest",
"operator": "=",
"value": "test",
},
],
"hide": "dontHide",
"label": "test-label",
"name": "test",
"skipUrlSync": false,
},
}
`);
});
it('should handle AdHocFiltersVariable with defaultKeys', () => {
const variable = new AdHocFiltersVariable({
name: 'test',
label: 'test-label',
description: 'test-desc',
datasource: { uid: 'fake-std', type: 'fake-std' },
defaultKeys: [
{
text: 'some',
value: '1',
},
{
text: 'static',
value: '2',
},
{
text: 'keys',
value: '3',
},
],
filters: [
{
key: 'filterTest',
operator: '=',
value: 'test',
},
],
baseFilters: [
{
key: 'baseFilterTest',
operator: '=',
value: 'test',
},
],
});
const set = new SceneVariableSet({
variables: [variable],
});
const result = sceneVariablesSetToSchemaV2Variables(set);
expect(result).toHaveLength(1);
expect(result[0]).toMatchInlineSnapshot(`
{
"kind": "AdhocVariable",
"spec": {
"baseFilters": [
{
"key": "baseFilterTest",
"operator": "=",
"value": "test",
},
],
"datasource": {
"type": "fake-std",
"uid": "fake-std",
},
"defaultKeys": [
{
"text": "some",
"value": "1",
},
{
"text": "static",
"value": "2",
},
{
"text": "keys",
"value": "3",
},
],
"description": "test-desc",
"filters": [
{
"key": "filterTest",
"operator": "=",
"value": "test",
},
],
"hide": "dontHide",
"label": "test-label",
"name": "test",
"skipUrlSync": false,
},
}
`);
});
describe('when the groupByVariable feature toggle is enabled', () => {
beforeAll(() => {
config.featureToggles.groupByVariable = true;
});
afterAll(() => {
config.featureToggles.groupByVariable = false;
});
it('should handle GroupByVariable', () => {
const variable = new GroupByVariable({
name: 'test',
label: 'test-label',
description: 'test-desc',
datasource: { uid: 'fake-std', type: 'fake-std' },
defaultOptions: [
{
text: 'Foo',
value: 'foo',
},
{
text: 'Bar',
value: 'bar',
},
],
});
const set = new SceneVariableSet({
variables: [variable],
});
const result = sceneVariablesSetToSchemaV2Variables(set);
expect(result).toHaveLength(1);
expect(result[0]).toMatchInlineSnapshot(`
{
"kind": "GroupByVariable",
"spec": {
"current": {
"text": [],
"value": [],
},
"datasource": {
"type": "fake-std",
"uid": "fake-std",
},
"description": "test-desc",
"hide": "dontHide",
"includeAll": false,
"label": "test-label",
"multi": true,
"name": "test",
"options": [
{
"text": "Foo",
"value": "foo",
},
{
"text": "Bar",
"value": "bar",
},
],
"skipUrlSync": false,
},
}
`);
});
});
describe('when the groupByVariable feature toggle is disabled', () => {
it('should not handle GroupByVariable and throw an error', () => {
const variable = new GroupByVariable({
name: 'test',
label: 'test-label',
description: 'test-desc',
datasource: { uid: 'fake-std', type: 'fake-std' },
defaultOptions: [
{
text: 'Foo',
value: 'foo',
},
{
text: 'Bar',
value: 'bar',
},
],
});
const set = new SceneVariableSet({
variables: [variable],
});
expect(() => sceneVariablesSetToSchemaV2Variables(set)).toThrow('Unsupported variable type');
});
});
});
});

View File

@ -1,9 +1,34 @@
import { config } from '@grafana/runtime';
import { MultiValueVariable, SceneVariables, sceneUtils } from '@grafana/scenes';
import { VariableHide, VariableModel, VariableOption, VariableRefresh, VariableSort } from '@grafana/schema';
import {
VariableModel,
VariableRefresh as OldVariableRefresh,
VariableHide as OldVariableHide,
VariableSort as OldVariableSort,
} from '@grafana/schema';
import {
AdhocVariableKind,
ConstantVariableKind,
CustomVariableKind,
DataQueryKind,
DatasourceVariableKind,
IntervalVariableKind,
QueryVariableKind,
TextVariableKind,
GroupByVariableKind,
defaultVariableHide,
VariableOption,
VariableRefresh,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen';
import { getIntervalsQueryFromNewIntervalModel } from '../utils/utils';
import { getDataQueryKind, getDataQuerySpec } from './transformSceneToSaveModelSchemaV2';
import {
transformVariableRefreshToEnum,
transformVariableHideToEnum,
transformSortVariableToEnum,
} from './transformToV2TypesUtils';
/**
* Converts a SceneVariables object into an array of VariableModel objects.
* @param set - The SceneVariables object containing the variables to convert.
@ -21,14 +46,14 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
label: variable.state.label,
description: variable.state.description ?? undefined,
skipUrlSync: Boolean(variable.state.skipUrlSync),
hide: variable.state.hide || VariableHide.dontHide,
hide: variable.state.hide || OldVariableHide.dontHide,
type: variable.state.type,
};
if (sceneUtils.isQueryVariable(variable)) {
let options: VariableOption[] = [];
// Not sure if we actually have to still support this option given
// that it's not exposed in the UI
if (variable.state.refresh === VariableRefresh.never || keepQueryOptions) {
if (transformVariableRefreshToEnum(variable.state.refresh) === VariableRefresh.Never || keepQueryOptions) {
options = variableValueOptionsToVariableOptions(variable.state);
}
variables.push({
@ -77,7 +102,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
},
options: [],
regex: variable.state.regex,
refresh: VariableRefresh.onDashboardLoad,
refresh: OldVariableRefresh.onDashboardLoad,
query: variable.state.pluginId,
multi: variable.state.isMulti,
allValue: variable.state.allValue,
@ -94,7 +119,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
},
// @ts-expect-error
query: variable.state.value,
hide: VariableHide.hideVariable,
hide: OldVariableHide.hideVariable,
});
} else if (sceneUtils.isIntervalVariable(variable)) {
const intervals = getIntervalsQueryFromNewIntervalModel(variable.state.intervals);
@ -162,7 +187,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
// Remove some defaults
for (const variable of variables) {
if (variable.hide === VariableHide.dontHide) {
if (variable.hide === OldVariableHide.dontHide) {
delete variable.hide;
}
@ -178,7 +203,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
delete variable.multi;
}
if (variable.sort === VariableSort.disabled) {
if (variable.sort === OldVariableSort.disabled) {
delete variable.sort;
}
}
@ -193,3 +218,201 @@ function variableValueOptionsToVariableOptions(varState: MultiValueVariable['sta
selected: Array.isArray(varState.value) ? varState.value.includes(o.value) : varState.value === o.value,
}));
}
export function sceneVariablesSetToSchemaV2Variables(
set: SceneVariables,
keepQueryOptions?: boolean
): Array<
| QueryVariableKind
| TextVariableKind
| IntervalVariableKind
| DatasourceVariableKind
| CustomVariableKind
| ConstantVariableKind
| GroupByVariableKind
| AdhocVariableKind
> {
let variables: Array<
| QueryVariableKind
| TextVariableKind
| IntervalVariableKind
| DatasourceVariableKind
| CustomVariableKind
| ConstantVariableKind
| GroupByVariableKind
| AdhocVariableKind
> = [];
for (const variable of set.state.variables) {
const commonProperties = {
name: variable.state.name,
label: variable.state.label,
description: variable.state.description ?? undefined,
skipUrlSync: Boolean(variable.state.skipUrlSync),
hide: transformVariableHideToEnum(variable.state.hide) || defaultVariableHide(),
};
// current: VariableOption;
const currentVariableOption: VariableOption = {
// @ts-expect-error
value: variable.state.value,
// @ts-expect-error
text: variable.state.text,
};
let options: VariableOption[] = [];
if (sceneUtils.isQueryVariable(variable)) {
// Not sure if we actually have to still support this option given
// that it's not exposed in the UI
if (transformVariableRefreshToEnum(variable.state.refresh) === VariableRefresh.Never || keepQueryOptions) {
options = variableValueOptionsToVariableOptions(variable.state);
}
//query: DataQueryKind | string;
const query = variable.state.query;
let dataQuery: DataQueryKind | string;
if (typeof query !== 'string') {
dataQuery = {
kind: getDataQueryKind(query),
spec: getDataQuerySpec(query),
};
} else {
dataQuery = query;
}
const queryVariable: QueryVariableKind = {
kind: 'QueryVariable',
spec: {
...commonProperties,
current: currentVariableOption,
options,
query: dataQuery,
definition: variable.state.definition,
datasource: variable.state.datasource || {},
sort: transformSortVariableToEnum(variable.state.sort),
refresh: transformVariableRefreshToEnum(variable.state.refresh),
regex: variable.state.regex,
allValue: variable.state.allValue,
includeAll: variable.state.includeAll || false,
multi: variable.state.isMulti || false,
skipUrlSync: variable.state.skipUrlSync || false,
},
};
variables.push(queryVariable);
} else if (sceneUtils.isCustomVariable(variable)) {
options = variableValueOptionsToVariableOptions(variable.state);
const customVariable: CustomVariableKind = {
kind: 'CustomVariable',
spec: {
...commonProperties,
current: currentVariableOption,
options,
query: variable.state.query,
multi: variable.state.isMulti || false,
allValue: variable.state.allValue,
includeAll: variable.state.includeAll ?? false,
},
};
variables.push(customVariable);
} else if (sceneUtils.isDataSourceVariable(variable)) {
const datasourceVariable: DatasourceVariableKind = {
kind: 'DatasourceVariable',
spec: {
...commonProperties,
current: currentVariableOption,
options: [],
regex: variable.state.regex,
refresh: VariableRefresh.OnDashboardLoad,
pluginId: variable.state.pluginId,
defaultOptionEnabled: !!variable.state.defaultOptionEnabled,
multi: variable.state.isMulti || false,
allValue: variable.state.allValue,
includeAll: variable.state.includeAll || false,
},
};
variables.push(datasourceVariable);
} else if (sceneUtils.isConstantVariable(variable)) {
const constantVariable: ConstantVariableKind = {
kind: 'ConstantVariable',
spec: {
...commonProperties,
current: currentVariableOption,
// @ts-expect-error
query: variable.state.value,
},
};
variables.push(constantVariable);
} else if (sceneUtils.isIntervalVariable(variable)) {
const intervals = getIntervalsQueryFromNewIntervalModel(variable.state.intervals);
const intervalVariable: IntervalVariableKind = {
kind: 'IntervalVariable',
spec: {
...commonProperties,
current: currentVariableOption,
query: intervals,
refresh: VariableRefresh.OnTimeRangeChanged,
options: variable.state.intervals.map((interval) => ({
value: interval,
text: interval,
selected: interval === variable.state.value,
})),
auto: variable.state.autoEnabled,
auto_min: variable.state.autoMinInterval,
auto_count: variable.state.autoStepCount,
},
};
variables.push(intervalVariable);
} else if (sceneUtils.isTextBoxVariable(variable)) {
const current = {
text: variable.state.value,
value: variable.state.value,
};
const textBoxVariable: TextVariableKind = {
kind: 'TextVariable',
spec: {
...commonProperties,
current,
query: variable.state.value,
},
};
variables.push(textBoxVariable);
} else if (sceneUtils.isGroupByVariable(variable) && config.featureToggles.groupByVariable) {
options = variableValueOptionsToVariableOptions(variable.state);
const groupVariable: GroupByVariableKind = {
kind: 'GroupByVariable',
spec: {
...commonProperties,
datasource: variable.state.datasource || {}, // FIXME what is the default value?,
// Only persist the statically defined options
options:
variable.state.defaultOptions?.map((option) => ({
text: option.text,
value: String(option.value),
})) || [],
current: currentVariableOption,
multi: variable.state.isMulti || false,
includeAll: variable.state.includeAll || false,
},
};
variables.push(groupVariable);
} else if (sceneUtils.isAdHocVariable(variable)) {
const adhocVariable: AdhocVariableKind = {
kind: 'AdhocVariable',
spec: {
...commonProperties,
name: variable.state.name,
datasource: variable.state.datasource || {}, //FIXME what is the default value?
baseFilters: variable.state.baseFilters || [],
filters: variable.state.filters,
defaultKeys: variable.state.defaultKeys || [], //FIXME what is the default value?
},
};
variables.push(adhocVariable);
} else {
throw new Error('Unsupported variable type');
}
}
return variables;
}

View File

@ -0,0 +1,470 @@
import { behaviors, SceneDataQuery, SceneDataTransformer, SceneVariableSet, VizPanel } from '@grafana/scenes';
import {
DashboardV2Spec,
defaultDashboardV2Spec,
defaultFieldConfigSource,
PanelKind,
PanelQueryKind,
TransformationKind,
FieldConfigSource,
DashboardLink,
DashboardCursorSync,
DataTransformerConfig,
PanelQuerySpec,
DataQueryKind,
defaultDataSourceRef,
GridLayoutItemKind,
QueryOptionsSpec,
QueryVariableKind,
TextVariableKind,
IntervalVariableKind,
DatasourceVariableKind,
CustomVariableKind,
ConstantVariableKind,
GroupByVariableKind,
AdhocVariableKind,
} from '../../../../../packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.gen';
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getQueryRunnerFor } from '../utils/utils';
import { sceneVariablesSetToSchemaV2Variables } from './sceneVariablesSetToVariables';
import { transformDashboardLinksToEnums, transformCursorSynctoEnum } from './transformToV2TypesUtils';
// FIXME: This is temporary to avoid creating partial types for all the new schema, it has some performance implications, but it's fine for now
type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnapshot = false): Partial<DashboardV2Spec> {
const oldDash = scene.state;
const timeRange = oldDash.$timeRange!.state;
const controlsState = oldDash.controls?.state;
const refreshPicker = controlsState?.refreshPicker;
const dashboardSchemaV2: DeepPartial<DashboardV2Spec> = {
//dashboard settings
title: oldDash.title,
description: oldDash.description ?? '',
cursorSync: getCursorSync(oldDash),
liveNow: getLiveNow(oldDash),
preload: oldDash.preload,
editable: oldDash.editable,
links: transformDashboardLinksToEnums(oldDash.links),
tags: oldDash.tags,
// EOF dashboard settings
// time settings
timeSettings: {
timezone: timeRange.timeZone,
from: timeRange.from,
to: timeRange.to,
autoRefresh: refreshPicker?.state.refresh || '',
autoRefreshIntervals: refreshPicker?.state.intervals,
quickRanges: [], //FIXME is coming timepicker.time_options,
hideTimepicker: controlsState?.hideTimeControls ?? false,
weekStart: timeRange.weekStart,
fiscalYearStartMonth: timeRange.fiscalYearStartMonth,
nowDelay: timeRange.UNSAFE_nowDelay,
},
// EOF time settings
// variables
variables: getVariables(oldDash),
// EOF variables
// elements
elements: getElements(oldDash),
// EOF elements
// annotations
annotations: [], //FIXME
// EOF annotations
// layout
layout: {
kind: 'GridLayout',
spec: {
items: getGridLayoutItems(oldDash),
},
},
// EOF layout
};
if (isDashboardSchemaV2(dashboardSchemaV2)) {
return dashboardSchemaV2;
}
console.error('Error transforming dashboard to schema v2');
throw new Error('Error transforming dashboard to schema v2');
}
function getCursorSync(state: DashboardSceneState) {
const cursorSync = state.$behaviors?.find((b): b is behaviors.CursorSync => b instanceof behaviors.CursorSync)?.state
.sync;
return transformCursorSynctoEnum(cursorSync);
}
function getLiveNow(state: DashboardSceneState) {
const liveNow =
state.$behaviors?.find((b): b is behaviors.LiveNowTimer => b instanceof behaviors.LiveNowTimer)?.isEnabled ||
undefined;
// hack for validator
if (liveNow === undefined) {
return Boolean(defaultDashboardV2Spec().liveNow);
}
return Boolean(liveNow);
}
function getGridLayoutItems(state: DashboardSceneState, isSnapshot?: boolean): GridLayoutItemKind[] {
const body = state.body;
const elements: GridLayoutItemKind[] = [];
if (body instanceof DefaultGridLayoutManager) {
for (const child of body.state.grid.state.children) {
if (child instanceof DashboardGridItem) {
// TODO: handle panel repeater scenario
// if (child.state.variableName) {
// panels = panels.concat(panelRepeaterToPanels(child, isSnapshot));
// } else {
elements.push(gridItemToGridLayoutItemKind(child, isSnapshot));
// }
}
// TODO: OLD transformer code
// if (child instanceof SceneGridRow) {
// // Skip repeat clones or when generating a snapshot
// if (child.state.key!.indexOf('-clone-') > 0 && !isSnapshot) {
// continue;
// }
// gridRowToSaveModel(child, panels, isSnapshot);
// }
}
}
return elements;
}
export function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, isSnapshot = false): GridLayoutItemKind {
let elementGridItem: GridLayoutItemKind | undefined;
let x = 0,
y = 0,
width = 0,
height = 0;
let gridItem_ = gridItem;
if (!(gridItem_.state.body instanceof VizPanel)) {
throw new Error('DashboardGridItem body expected to be VizPanel');
}
// Get the grid position and size
height = (gridItem_.state.variableName ? gridItem_.state.itemHeight : gridItem_.state.height) ?? 0;
x = gridItem_.state.x ?? 0;
y = gridItem_.state.y ?? 0;
width = gridItem_.state.width ?? 0;
// FIXME: which name should we use for the element reference, key or something else ?
const elementName = gridItem_.state.body.state.key ?? 'DefaultName';
elementGridItem = {
kind: 'GridLayoutItem',
spec: {
x,
y,
width: width,
height: height,
element: {
kind: 'ElementReference',
name: elementName,
},
},
};
if (!elementGridItem) {
throw new Error('Unsupported grid item type');
}
return elementGridItem;
}
function getElements(state: DashboardSceneState) {
const panels = state.body.getVizPanels() ?? [];
const panelsArray = panels.reduce((acc: PanelKind[], vizPanel: VizPanel) => {
const elementSpec: PanelKind = {
kind: 'Panel',
spec: {
uid: vizPanel.state.key ?? '', // FIXME: why is key optional?
title: vizPanel.state.title,
description: vizPanel.state.description ?? '',
links: getPanelLinks(vizPanel),
data: {
kind: 'QueryGroup',
spec: {
queries: getVizPanelQueries(vizPanel),
transformations: getVizPanelTransformations(vizPanel),
queryOptions: getVizPanelQueryOptions(vizPanel),
},
},
vizConfig: {
kind: vizPanel.state.pluginId,
spec: {
pluginVersion: vizPanel.state.pluginVersion ?? '',
options: vizPanel.state.options,
fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? defaultFieldConfigSource(),
},
},
},
};
acc.push(elementSpec);
return acc;
}, []);
// create elements
const elements = createElements(panelsArray);
return elements;
}
function getPanelLinks(panel: VizPanel): DashboardLink[] {
const vizLinks = dashboardSceneGraph.getPanelLinks(panel);
if (vizLinks) {
return (vizLinks.state.rawLinks as DashboardLink[]) ?? [];
}
return [];
}
function getVizPanelQueries(vizPanel: VizPanel): PanelQueryKind[] {
const queries: PanelQueryKind[] = [];
const queryRunner = getQueryRunnerFor(vizPanel);
const vizPanelQueries = queryRunner?.state.queries;
const datasource = queryRunner?.state.datasource;
if (vizPanelQueries) {
vizPanelQueries.forEach((query) => {
const dataQuery: DataQueryKind = {
kind: getDataQueryKind(query),
spec: query,
};
const querySpec: PanelQuerySpec = {
datasource: datasource ?? defaultDataSourceRef(),
query: dataQuery,
refId: query.refId,
hidden: query.hidden,
};
queries.push({
kind: 'PanelQuery',
spec: querySpec,
});
});
}
return queries;
}
export function getDataQueryKind(query: SceneDataQuery): string {
// If the query has a datasource, use the datasource type, otherwise use 'default'
return query.datasource?.type ?? 'default';
}
export function getDataQuerySpec(query: SceneDataQuery): Record<string, any> {
const dataQuerySpec = {
kind: getDataQueryKind(query),
spec: query,
};
return dataQuerySpec;
}
function getVizPanelTransformations(vizPanel: VizPanel): TransformationKind[] {
let transformations: TransformationKind[] = [];
const dataProvider = vizPanel.state.$data;
if (dataProvider instanceof SceneDataTransformer) {
const transformationList = dataProvider.state.transformations;
if (transformationList.length === 0) {
return [];
}
transformationList.forEach((transformationItem) => {
const transformation = transformationItem as DataTransformerConfig;
const transformationSpec: DataTransformerConfig = {
id: transformation.id,
disabled: transformation.disabled,
filter: {
id: transformation.filter?.id ?? '',
options: transformation.filter?.options ?? {},
},
topic: transformation.topic,
options: transformation.options,
};
transformations.push({
kind: transformation.id,
spec: transformationSpec,
});
});
}
return transformations;
}
function getVizPanelQueryOptions(vizPanel: VizPanel): QueryOptionsSpec {
let queryOptions: QueryOptionsSpec = {};
const queryRunner = getQueryRunnerFor(vizPanel);
if (queryRunner) {
queryOptions.maxDataPoints = queryRunner.state.maxDataPoints;
if (queryRunner.state.cacheTimeout) {
queryOptions.cacheTimeout = queryRunner.state.cacheTimeout;
}
if (queryRunner.state.queryCachingTTL) {
queryOptions.queryCachingTTL = queryRunner.state.queryCachingTTL;
}
if (queryRunner.state.minInterval) {
queryOptions.interval = queryRunner.state.minInterval;
}
}
const panelTime = vizPanel.state.$timeRange;
if (panelTime instanceof PanelTimeRange) {
queryOptions.timeFrom = panelTime.state.timeFrom;
queryOptions.timeShift = panelTime.state.timeShift;
}
return queryOptions;
}
function createElements(panels: PanelKind[]): Record<string, PanelKind> {
return panels.reduce(
(acc, panel) => {
const key = panel.spec.uid;
acc[key] = panel;
return acc;
},
{} as Record<string, PanelKind>
);
}
function getVariables(oldDash: DashboardSceneState) {
const variablesSet = oldDash.$variables;
// variables is an array of all variables kind (union)
let variables: Array<
| QueryVariableKind
| TextVariableKind
| IntervalVariableKind
| DatasourceVariableKind
| CustomVariableKind
| ConstantVariableKind
| GroupByVariableKind
| AdhocVariableKind
> = [];
if (variablesSet instanceof SceneVariableSet) {
variables = sceneVariablesSetToSchemaV2Variables(variablesSet);
}
return variables;
}
// Function to know if the dashboard transformed is a valid DashboardV2Spec
function isDashboardSchemaV2(dash: any): dash is DashboardV2Spec {
if (typeof dash !== 'object' || dash === null) {
return false;
}
if (typeof dash.title !== 'string') {
return false;
}
if (typeof dash.description !== 'string') {
return false;
}
if (typeof dash.cursorSync !== 'string') {
return false;
}
if (!Object.values(DashboardCursorSync).includes(dash.cursorSync)) {
return false;
}
if (typeof dash.liveNow !== 'boolean') {
return false;
}
if (typeof dash.preload !== 'boolean') {
return false;
}
if (typeof dash.editable !== 'boolean') {
return false;
}
if (!Array.isArray(dash.links)) {
return false;
}
if (!Array.isArray(dash.tags)) {
return false;
}
if (dash.id !== undefined && typeof dash.id !== 'number') {
return false;
}
// Time settings
if (typeof dash.timeSettings !== 'object' || dash.timeSettings === null) {
return false;
}
if (typeof dash.timeSettings.timezone !== 'string') {
return false;
}
if (typeof dash.timeSettings.from !== 'string') {
return false;
}
if (typeof dash.timeSettings.to !== 'string') {
return false;
}
if (typeof dash.timeSettings.autoRefresh !== 'string') {
return false;
}
if (!Array.isArray(dash.timeSettings.autoRefreshIntervals)) {
return false;
}
if (!Array.isArray(dash.timeSettings.quickRanges)) {
return false;
}
if (typeof dash.timeSettings.hideTimepicker !== 'boolean') {
return false;
}
if (typeof dash.timeSettings.weekStart !== 'string') {
return false;
}
if (typeof dash.timeSettings.fiscalYearStartMonth !== 'number') {
return false;
}
if (dash.timeSettings.nowDelay !== undefined && typeof dash.timeSettings.nowDelay !== 'string') {
return false;
}
// Other sections
if (!Array.isArray(dash.variables)) {
return false;
}
if (typeof dash.elements !== 'object' || dash.elements === null) {
return false;
}
if (!Array.isArray(dash.annotations)) {
return false;
}
// Layout
if (typeof dash.layout !== 'object' || dash.layout === null) {
return false;
}
if (dash.layout.kind !== 'GridLayout') {
return false;
}
if (typeof dash.layout.spec !== 'object' || dash.layout.spec === null) {
return false;
}
if (!Array.isArray(dash.layout.spec.items)) {
return false;
}
return true;
}

View File

@ -0,0 +1,108 @@
import { DashboardLink as DashboardLinkTypeV1 } from '@grafana/schema';
import {
DashboardCursorSync,
DashboardLinkType,
VariableRefresh,
VariableHide,
VariableSort,
defaultVariableHide,
defaultVariableSort,
defaultVariableRefresh,
defaultDashboardLinkType,
defaultDashboardCursorSync,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen';
import {
transformCursorSynctoEnum,
transformDashboardLinksToEnums,
transformVariableRefreshToEnum,
transformVariableHideToEnum,
transformSortVariableToEnum,
} from './transformToV2TypesUtils';
describe('transformToV2TypesUtils', () => {
describe('transformCursorSynctoEnum', () => {
it('should return the correct enum value for cursor sync', () => {
expect(transformCursorSynctoEnum(0)).toBe(DashboardCursorSync.Off);
expect(transformCursorSynctoEnum(1)).toBe(DashboardCursorSync.Crosshair);
expect(transformCursorSynctoEnum(2)).toBe(DashboardCursorSync.Tooltip);
expect(transformCursorSynctoEnum(undefined)).toBe(defaultDashboardCursorSync());
});
});
describe('transformDashboardLinksToEnums', () => {
const links: DashboardLinkTypeV1[] = [
{
type: 'link',
asDropdown: false,
icon: '',
includeVars: false,
keepTime: false,
tags: [],
title: '',
url: '',
targetBlank: false,
tooltip: '',
},
{
type: 'dashboards',
asDropdown: false,
icon: '',
includeVars: false,
keepTime: false,
tags: [],
title: '',
url: '',
targetBlank: false,
tooltip: '',
},
{
// @ts-expect-error Testing invalid type
type: 'non-valid-type',
asDropdown: false,
icon: '',
includeVars: false,
keepTime: false,
tags: [],
title: '',
url: '',
targetBlank: false,
tooltip: '',
},
];
const transformedLinks = transformDashboardLinksToEnums(links);
expect(transformedLinks[0].type).toBe(DashboardLinkType.Link);
expect(transformedLinks[1].type).toBe(DashboardLinkType.Dashboards);
expect(transformedLinks[2].type).toBe(defaultDashboardLinkType());
});
});
describe('transformVariableRefreshToEnum', () => {
it('should return the correct enum value for variable refresh', () => {
expect(transformVariableRefreshToEnum(0)).toBe(VariableRefresh.Never);
expect(transformVariableRefreshToEnum(1)).toBe(VariableRefresh.OnDashboardLoad);
expect(transformVariableRefreshToEnum(2)).toBe(VariableRefresh.OnTimeRangeChanged);
expect(transformVariableRefreshToEnum(undefined)).toBe(defaultVariableRefresh());
});
});
describe('transformVariableHideToEnum', () => {
it('should return the correct enum value for variable hide', () => {
expect(transformVariableHideToEnum(0)).toBe(VariableHide.DontHide);
expect(transformVariableHideToEnum(1)).toBe(VariableHide.HideLabel);
expect(transformVariableHideToEnum(2)).toBe(VariableHide.HideVariable);
expect(transformVariableHideToEnum(undefined)).toBe(defaultVariableHide());
});
});
describe('transformSortVariableToEnum', () => {
it('should return the correct enum value for variable sort', () => {
expect(transformSortVariableToEnum(0)).toBe(VariableSort.Disabled);
expect(transformSortVariableToEnum(1)).toBe(VariableSort.AlphabeticalAsc);
expect(transformSortVariableToEnum(2)).toBe(VariableSort.AlphabeticalDesc);
expect(transformSortVariableToEnum(3)).toBe(VariableSort.NumericalAsc);
expect(transformSortVariableToEnum(4)).toBe(VariableSort.NumericalDesc);
expect(transformSortVariableToEnum(undefined)).toBe(defaultVariableSort());
});
});

View File

@ -0,0 +1,94 @@
import {
DashboardLink as DashboardLinkV1,
VariableHide as VariableHideV1,
VariableRefresh as VariableRefreshV1,
VariableSort as VariableSortV1,
DashboardCursorSync as DashboardCursorSyncV1,
DashboardLinkType as DashboardLinkTypeV1,
} from '@grafana/schema';
import {
DashboardCursorSync,
defaultDashboardV2Spec,
DashboardLinkType,
DashboardLink,
defaultVariableHide,
defaultVariableRefresh,
defaultVariableSort,
VariableHide,
VariableRefresh,
VariableSort,
defaultDashboardLinkType,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen';
export function transformCursorSynctoEnum(cursorSync?: DashboardCursorSyncV1): DashboardCursorSync {
switch (cursorSync) {
case 0:
return DashboardCursorSync.Off;
case 1:
return DashboardCursorSync.Crosshair;
case 2:
return DashboardCursorSync.Tooltip;
default:
return defaultDashboardV2Spec().cursorSync;
}
}
function transformDashboardLinkTypeToEnum(linkType: DashboardLinkTypeV1): DashboardLinkType {
switch (linkType) {
case 'link':
return DashboardLinkType.Link;
case 'dashboards':
return DashboardLinkType.Dashboards;
default:
return defaultDashboardLinkType();
}
}
export function transformDashboardLinksToEnums(links: DashboardLinkV1[]): DashboardLink[] {
return links.map((link) => {
return {
...link,
type: transformDashboardLinkTypeToEnum(link.type),
};
});
}
export function transformVariableRefreshToEnum(refresh?: VariableRefreshV1): VariableRefresh {
switch (refresh) {
case 0:
return VariableRefresh.Never;
case 1:
return VariableRefresh.OnDashboardLoad;
case 2:
return VariableRefresh.OnTimeRangeChanged;
default:
return defaultVariableRefresh();
}
}
export function transformVariableHideToEnum(hide?: VariableHideV1): VariableHide {
switch (hide) {
case 0:
return VariableHide.DontHide;
case 1:
return VariableHide.HideLabel;
case 2:
return VariableHide.HideVariable;
default:
return defaultVariableHide();
}
}
export function transformSortVariableToEnum(sort?: VariableSortV1): VariableSort {
switch (sort) {
case 0:
return VariableSort.Disabled;
case 1:
return VariableSort.AlphabeticalAsc;
case 2:
return VariableSort.AlphabeticalDesc;
case 3:
return VariableSort.NumericalAsc;
case 4:
return VariableSort.NumericalDesc;
default:
return defaultVariableSort();
}
}

View File

@ -2,6 +2,7 @@ import saveAs from 'file-saver';
import { useAsync } from 'react-use';
import AutoSizer from 'react-virtualized-auto-sizer';
import { config } from '@grafana/runtime';
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
import { Button, ClipboardButton, CodeEditor, Field, Modal, Stack, Switch } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
@ -10,6 +11,7 @@ import { shareDashboardType } from 'app/features/dashboard/components/ShareModal
import { DashboardModel } from 'app/features/dashboard/state';
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
import { transformSceneToSaveModelSchemaV2 } from '../serialization/transformSceneToSaveModelSchemaV2';
import { getVariablesCompatibility } from '../utils/getVariablesCompatibility';
import { DashboardInteractions } from '../utils/interactions';
import { getDashboardSceneFor } from '../utils/utils';
@ -19,6 +21,7 @@ import { SceneShareTabState, ShareView } from './types';
export interface ShareExportTabState extends SceneShareTabState {
isSharingExternally?: boolean;
isViewingJSON?: boolean;
isViewingJSONSchemaV2?: boolean;
}
export class ShareExportTab extends SceneObjectBase<ShareExportTabState> implements ShareView {
@ -51,6 +54,12 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
});
};
public onViewJSONSchemaV2 = () => {
this.setState({
isViewingJSONSchemaV2: !this.state.isViewingJSONSchemaV2,
});
};
public getClipboardText() {
return;
}
@ -72,6 +81,23 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
return exportable;
};
public getExportableDashboardJsonSchemaV2 = async () => {
// const { isSharingExternally } = this.state;
const saveModel = transformSceneToSaveModelSchemaV2(getDashboardSceneFor(this));
// const exportable = isSharingExternally
// ? await this._exporter.makeExportable(
// new DashboardModel(saveModel, undefined, {
// getVariablesFromState: () => {
// return getVariablesCompatibility(window.__grafanaSceneContext);
// },
// })
// )
// : saveModel;
return saveModel;
};
public onSaveAsFile = async () => {
const dashboardJson = await this.getExportableDashboardJson();
const dashboardJsonPretty = JSON.stringify(dashboardJson, null, 2);
@ -94,8 +120,9 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
}
function ShareExportTabRenderer({ model }: SceneComponentProps<ShareExportTab>) {
const { isSharingExternally, isViewingJSON, modalRef } = model.useState();
const { isSharingExternally, isViewingJSON, modalRef, isViewingJSONSchemaV2 } = model.useState();
// use dashboardSchemaV2 to show new button, this is for internal testing
const shouldShowNewSchemaV2Button = config.featureToggles.dashboardSchemaV2 ?? false;
const dashboardJson = useAsync(async () => {
if (isViewingJSON) {
const json = await model.getExportableDashboardJson();
@ -105,11 +132,20 @@ function ShareExportTabRenderer({ model }: SceneComponentProps<ShareExportTab>)
return '';
}, [isViewingJSON]);
const dashboardJsonSchemaV2 = useAsync(async () => {
if (isViewingJSONSchemaV2) {
const json = await model.getExportableDashboardJsonSchemaV2();
console.log('json v2', json);
return JSON.stringify(json, null, 2);
}
return '';
}, [isViewingJSONSchemaV2]);
const exportExternallyTranslation = t('share-modal.export.share-externally-label', `Export for sharing externally`);
return (
<>
{!isViewingJSON && (
{!isViewingJSON && !isViewingJSONSchemaV2 && (
<>
<p>
<Trans i18nKey="share-modal.export.info-text">Export this dashboard.</Trans>
@ -137,13 +173,17 @@ function ShareExportTabRenderer({ model }: SceneComponentProps<ShareExportTab>)
<Button variant="secondary" icon="brackets-curly" onClick={model.onViewJSON}>
<Trans i18nKey="share-modal.export.view-button">View JSON</Trans>
</Button>
{shouldShowNewSchemaV2Button && (
<Button variant="secondary" icon="brackets-curly" onClick={model.onViewJSONSchemaV2}>
<Trans i18nKey="share-modal.export.view-button-schemav2">View JSON SchemaV2</Trans>
</Button>
)}
<Button variant="primary" icon="save" onClick={() => model.onSaveAsFile()}>
<Trans i18nKey="share-modal.export.save-button">Save to file</Trans>
</Button>
</Modal.ButtonRow>
</>
)}
{isViewingJSON && (
<>
<AutoSizer disableHeight>
@ -161,7 +201,12 @@ function ShareExportTabRenderer({ model }: SceneComponentProps<ShareExportTab>)
}
if (dashboardJson.loading) {
return <div>Loading...</div>;
return (
<div>
{' '}
<Trans i18nKey="share-modal.export.loading">Loading...</Trans>
</div>
);
}
return null;
@ -186,6 +231,52 @@ function ShareExportTabRenderer({ model }: SceneComponentProps<ShareExportTab>)
</Modal.ButtonRow>
</>
)}
{isViewingJSONSchemaV2 && (
<>
<AutoSizer disableHeight>
{({ width }) => {
if (dashboardJsonSchemaV2.value) {
return (
<CodeEditor
value={dashboardJsonSchemaV2.value ?? ''}
language="json"
showMiniMap={false}
height="500px"
width={width}
/>
);
}
if (dashboardJsonSchemaV2.loading) {
return (
<div>
<Trans i18nKey="share-modal.export.loading">Loading...</Trans>
</div>
);
}
return null;
}}
</AutoSizer>
<Modal.ButtonRow>
<Button variant="secondary" fill="outline" onClick={model.onViewJSONSchemaV2} icon="arrow-left">
<Trans i18nKey="share-modal.export.back-button">Back to export config</Trans>
</Button>
<ClipboardButton
variant="secondary"
icon="copy"
disabled={dashboardJson.loading}
getText={() => dashboardJson.value ?? ''}
>
<Trans i18nKey="share-modal.view-json.copy-button">Copy to Clipboard</Trans>
</ClipboardButton>
<Button variant="primary" icon="save" disabled={dashboardJson.loading} onClick={() => model.onSaveAsFile()}>
<Trans i18nKey="share-modal.export.save-button">Save to file</Trans>
</Button>
</Modal.ButtonRow>
</>
)}
</>
);
}

View File

@ -2649,9 +2649,11 @@
"back-button": "Back to export config",
"cancel-button": "Cancel",
"info-text": "Export this dashboard.",
"loading": "Loading...",
"save-button": "Save to file",
"share-externally-label": "Export for sharing externally",
"view-button": "View JSON"
"view-button": "View JSON",
"view-button-schemav2": "View JSON SchemaV2"
},
"library": {
"info": "Create library panel."

View File

@ -2649,9 +2649,11 @@
"back-button": "ßäčĸ ŧő ęχpőřŧ čőʼnƒįģ",
"cancel-button": "Cäʼnčęľ",
"info-text": "Ēχpőřŧ ŧĥįş đäşĥþőäřđ.",
"loading": "Ŀőäđįʼnģ...",
"save-button": "Ŝävę ŧő ƒįľę",
"share-externally-label": "Ēχpőřŧ ƒőř şĥäřįʼnģ ęχŧęřʼnäľľy",
"view-button": "Vįęŵ ĴŜØŃ"
"view-button": "Vįęŵ ĴŜØŃ",
"view-button-schemav2": "Vįęŵ ĴŜØŃ ŜčĥęmäV2"
},
"library": {
"info": "Cřęäŧę ľįþřäřy päʼnęľ."