From d2b788eb5395040d27aff07d0379895786947827 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Tue, 13 Jan 2026 13:41:40 +0000 Subject: [PATCH 01/19] Plugins: Remove angular details from meta API (#116194) remove angular details from meta API --- apps/plugins/kinds/meta.cue | 3 --- .../pkg/apis/plugins/v0alpha1/meta_spec_gen.go | 11 ----------- apps/plugins/pkg/apis/plugins_manifest.go | 2 +- apps/plugins/pkg/app/meta/converter.go | 16 +--------------- 4 files changed, 2 insertions(+), 30 deletions(-) diff --git a/apps/plugins/kinds/meta.cue b/apps/plugins/kinds/meta.cue index 01dc45adf77..479a9111d24 100644 --- a/apps/plugins/kinds/meta.cue +++ b/apps/plugins/kinds/meta.cue @@ -18,9 +18,6 @@ metaV0Alpha1: { type?: "grafana" | "commercial" | "community" | "private" | "private-glob" org?: string } - angular?: { - detected: bool - } translations?: [string]: string // +listType=atomic children?: [...string] diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/meta_spec_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_spec_gen.go index 631febbe2fa..141e9e5ad82 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/meta_spec_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_spec_gen.go @@ -215,7 +215,6 @@ type MetaSpec struct { Module *MetaV0alpha1SpecModule `json:"module,omitempty"` BaseURL *string `json:"baseURL,omitempty"` Signature *MetaV0alpha1SpecSignature `json:"signature,omitempty"` - Angular *MetaV0alpha1SpecAngular `json:"angular,omitempty"` Translations map[string]string `json:"translations,omitempty"` // +listType=atomic Children []string `json:"children,omitempty"` @@ -461,16 +460,6 @@ func NewMetaV0alpha1SpecSignature() *MetaV0alpha1SpecSignature { return &MetaV0alpha1SpecSignature{} } -// +k8s:openapi-gen=true -type MetaV0alpha1SpecAngular struct { - Detected bool `json:"detected"` -} - -// NewMetaV0alpha1SpecAngular creates a new MetaV0alpha1SpecAngular object. -func NewMetaV0alpha1SpecAngular() *MetaV0alpha1SpecAngular { - return &MetaV0alpha1SpecAngular{} -} - // +k8s:openapi-gen=true type MetaJSONDataType string diff --git a/apps/plugins/pkg/apis/plugins_manifest.go b/apps/plugins/pkg/apis/plugins_manifest.go index 0c52665e75d..f37c14ed0cf 100644 --- a/apps/plugins/pkg/apis/plugins_manifest.go +++ b/apps/plugins/pkg/apis/plugins_manifest.go @@ -23,7 +23,7 @@ var ( rawSchemaPluginv0alpha1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Plugin":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"id":{"type":"string"},"url":{"type":"string"},"version":{"type":"string"}},"required":["id","version"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) versionSchemaPluginv0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaPluginv0alpha1, &versionSchemaPluginv0alpha1) - rawSchemaMetav0alpha1 = []byte(`{"Dependencies":{"additionalProperties":false,"properties":{"extensions":{"additionalProperties":false,"properties":{"exposedComponents":{"description":"+listType=set","items":{"type":"string"},"type":"array"}},"type":"object"},"grafanaDependency":{"description":"Required field","type":"string"},"grafanaVersion":{"description":"Optional fields","type":"string"},"plugins":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"enum":["app","datasource","panel"],"type":"string"}},"required":["id","type","name"],"type":"object"},"type":"array"}},"required":["grafanaDependency"],"type":"object"},"EnterpriseFeatures":{"additionalProperties":false,"properties":{"healthDiagnosticsErrors":{"default":false,"description":"Allow additional properties","type":"boolean"}},"type":"object"},"Extensions":{"additionalProperties":false,"properties":{"addedComponents":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"addedFunctions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"addedLinks":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"exposedComponents":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"},"extensionPoints":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"}},"type":"object"},"IAM":{"additionalProperties":false,"properties":{"permissions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"action":{"type":"string"},"scope":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"Include":{"additionalProperties":false,"properties":{"action":{"type":"string"},"addToNav":{"type":"boolean"},"component":{"type":"string"},"defaultNav":{"type":"boolean"},"icon":{"type":"string"},"name":{"type":"string"},"path":{"type":"string"},"role":{"enum":["Admin","Editor","Viewer","None"],"type":"string"},"type":{"enum":["dashboard","page","panel","datasource"],"type":"string"},"uid":{"type":"string"}},"type":"object"},"Info":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"description":"Optional fields","properties":{"email":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"description":{"type":"string"},"keywords":{"description":"Required fields\n+listType=set","items":{"type":"string"},"type":"array"},"links":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"type":"array"},"logos":{"additionalProperties":false,"properties":{"large":{"type":"string"},"small":{"type":"string"}},"required":["small","large"],"type":"object"},"screenshots":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"path":{"type":"string"}},"type":"object"},"type":"array"},"updated":{"type":"string"},"version":{"type":"string"}},"required":["keywords","logos","updated","version"],"type":"object"},"JSONData":{"additionalProperties":false,"description":"JSON configuration schema for Grafana plugins\nConverted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json","properties":{"alerting":{"description":"Optional fields","type":"boolean"},"annotations":{"type":"boolean"},"autoEnabled":{"type":"boolean"},"backend":{"type":"boolean"},"buildMode":{"type":"string"},"builtIn":{"type":"boolean"},"category":{"enum":["tsdb","logging","cloud","tracing","profiling","sql","enterprise","iot","other"],"type":"string"},"dependencies":{"$ref":"#/components/schemas/Dependencies","description":"Dependency information"},"enterpriseFeatures":{"$ref":"#/components/schemas/EnterpriseFeatures"},"executable":{"type":"string"},"extensions":{"$ref":"#/components/schemas/Extensions"},"hideFromList":{"type":"boolean"},"iam":{"$ref":"#/components/schemas/IAM"},"id":{"description":"Unique name of the plugin","type":"string"},"includes":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Include"},"type":"array"},"info":{"$ref":"#/components/schemas/Info","description":"Metadata for the plugin"},"logs":{"type":"boolean"},"metrics":{"type":"boolean"},"multiValueFilterOperators":{"type":"boolean"},"name":{"description":"Human-readable name of the plugin","type":"string"},"pascalName":{"type":"string"},"preload":{"type":"boolean"},"queryOptions":{"$ref":"#/components/schemas/QueryOptions"},"roles":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Role"},"type":"array"},"routes":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Route"},"type":"array"},"skipDataQuery":{"type":"boolean"},"state":{"enum":["alpha","beta"],"type":"string"},"streaming":{"type":"boolean"},"suggestions":{"type":"boolean"},"tracing":{"type":"boolean"},"type":{"description":"Plugin type","enum":["app","datasource","panel","renderer"],"type":"string"}},"required":["id","type","name","info","dependencies"],"type":"object"},"Meta":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"QueryOptions":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"boolean"},"maxDataPoints":{"type":"boolean"},"minInterval":{"type":"boolean"}},"type":"object"},"Role":{"additionalProperties":false,"properties":{"grants":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"role":{"additionalProperties":false,"properties":{"description":{"type":"string"},"name":{"type":"string"},"permissions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"action":{"type":"string"},"scope":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"}},"type":"object"},"Route":{"additionalProperties":false,"properties":{"body":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"headers":{"description":"+listType=atomic","items":{"type":"string"},"type":"array"},"jwtTokenAuth":{"additionalProperties":false,"properties":{"params":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"scopes":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"url":{"type":"string"}},"type":"object"},"method":{"type":"string"},"path":{"type":"string"},"reqAction":{"type":"string"},"reqRole":{"type":"string"},"reqSignedIn":{"type":"boolean"},"tokenAuth":{"additionalProperties":false,"properties":{"params":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"scopes":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"url":{"type":"string"}},"type":"object"},"url":{"type":"string"},"urlParams":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"content":{"type":"string"},"name":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"spec":{"additionalProperties":false,"properties":{"angular":{"additionalProperties":false,"properties":{"detected":{"type":"boolean"}},"required":["detected"],"type":"object"},"baseURL":{"type":"string"},"children":{"description":"+listType=atomic","items":{"type":"string"},"type":"array"},"class":{"enum":["core","external"],"type":"string"},"module":{"additionalProperties":false,"properties":{"hash":{"type":"string"},"loadingStrategy":{"enum":["fetch","script"],"type":"string"},"path":{"type":"string"}},"required":["path"],"type":"object"},"pluginJson":{"$ref":"#/components/schemas/JSONData"},"signature":{"additionalProperties":false,"properties":{"org":{"type":"string"},"status":{"enum":["internal","valid","invalid","modified","unsigned"],"type":"string"},"type":{"enum":["grafana","commercial","community","private","private-glob"],"type":"string"}},"required":["status"],"type":"object"},"translations":{"additionalProperties":{"type":"string"},"type":"object"}},"required":["pluginJson","class"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + rawSchemaMetav0alpha1 = []byte(`{"Dependencies":{"additionalProperties":false,"properties":{"extensions":{"additionalProperties":false,"properties":{"exposedComponents":{"description":"+listType=set","items":{"type":"string"},"type":"array"}},"type":"object"},"grafanaDependency":{"description":"Required field","type":"string"},"grafanaVersion":{"description":"Optional fields","type":"string"},"plugins":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"enum":["app","datasource","panel"],"type":"string"}},"required":["id","type","name"],"type":"object"},"type":"array"}},"required":["grafanaDependency"],"type":"object"},"EnterpriseFeatures":{"additionalProperties":false,"properties":{"healthDiagnosticsErrors":{"default":false,"description":"Allow additional properties","type":"boolean"}},"type":"object"},"Extensions":{"additionalProperties":false,"properties":{"addedComponents":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"addedFunctions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"addedLinks":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"exposedComponents":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"},"extensionPoints":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"}},"type":"object"},"IAM":{"additionalProperties":false,"properties":{"permissions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"action":{"type":"string"},"scope":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"Include":{"additionalProperties":false,"properties":{"action":{"type":"string"},"addToNav":{"type":"boolean"},"component":{"type":"string"},"defaultNav":{"type":"boolean"},"icon":{"type":"string"},"name":{"type":"string"},"path":{"type":"string"},"role":{"enum":["Admin","Editor","Viewer","None"],"type":"string"},"type":{"enum":["dashboard","page","panel","datasource"],"type":"string"},"uid":{"type":"string"}},"type":"object"},"Info":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"description":"Optional fields","properties":{"email":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"description":{"type":"string"},"keywords":{"description":"Required fields\n+listType=set","items":{"type":"string"},"type":"array"},"links":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"type":"array"},"logos":{"additionalProperties":false,"properties":{"large":{"type":"string"},"small":{"type":"string"}},"required":["small","large"],"type":"object"},"screenshots":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"path":{"type":"string"}},"type":"object"},"type":"array"},"updated":{"type":"string"},"version":{"type":"string"}},"required":["keywords","logos","updated","version"],"type":"object"},"JSONData":{"additionalProperties":false,"description":"JSON configuration schema for Grafana plugins\nConverted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json","properties":{"alerting":{"description":"Optional fields","type":"boolean"},"annotations":{"type":"boolean"},"autoEnabled":{"type":"boolean"},"backend":{"type":"boolean"},"buildMode":{"type":"string"},"builtIn":{"type":"boolean"},"category":{"enum":["tsdb","logging","cloud","tracing","profiling","sql","enterprise","iot","other"],"type":"string"},"dependencies":{"$ref":"#/components/schemas/Dependencies","description":"Dependency information"},"enterpriseFeatures":{"$ref":"#/components/schemas/EnterpriseFeatures"},"executable":{"type":"string"},"extensions":{"$ref":"#/components/schemas/Extensions"},"hideFromList":{"type":"boolean"},"iam":{"$ref":"#/components/schemas/IAM"},"id":{"description":"Unique name of the plugin","type":"string"},"includes":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Include"},"type":"array"},"info":{"$ref":"#/components/schemas/Info","description":"Metadata for the plugin"},"logs":{"type":"boolean"},"metrics":{"type":"boolean"},"multiValueFilterOperators":{"type":"boolean"},"name":{"description":"Human-readable name of the plugin","type":"string"},"pascalName":{"type":"string"},"preload":{"type":"boolean"},"queryOptions":{"$ref":"#/components/schemas/QueryOptions"},"roles":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Role"},"type":"array"},"routes":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Route"},"type":"array"},"skipDataQuery":{"type":"boolean"},"state":{"enum":["alpha","beta"],"type":"string"},"streaming":{"type":"boolean"},"suggestions":{"type":"boolean"},"tracing":{"type":"boolean"},"type":{"description":"Plugin type","enum":["app","datasource","panel","renderer"],"type":"string"}},"required":["id","type","name","info","dependencies"],"type":"object"},"Meta":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"QueryOptions":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"boolean"},"maxDataPoints":{"type":"boolean"},"minInterval":{"type":"boolean"}},"type":"object"},"Role":{"additionalProperties":false,"properties":{"grants":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"role":{"additionalProperties":false,"properties":{"description":{"type":"string"},"name":{"type":"string"},"permissions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"action":{"type":"string"},"scope":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"}},"type":"object"},"Route":{"additionalProperties":false,"properties":{"body":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"headers":{"description":"+listType=atomic","items":{"type":"string"},"type":"array"},"jwtTokenAuth":{"additionalProperties":false,"properties":{"params":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"scopes":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"url":{"type":"string"}},"type":"object"},"method":{"type":"string"},"path":{"type":"string"},"reqAction":{"type":"string"},"reqRole":{"type":"string"},"reqSignedIn":{"type":"boolean"},"tokenAuth":{"additionalProperties":false,"properties":{"params":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"scopes":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"url":{"type":"string"}},"type":"object"},"url":{"type":"string"},"urlParams":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"content":{"type":"string"},"name":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"spec":{"additionalProperties":false,"properties":{"baseURL":{"type":"string"},"children":{"description":"+listType=atomic","items":{"type":"string"},"type":"array"},"class":{"enum":["core","external"],"type":"string"},"module":{"additionalProperties":false,"properties":{"hash":{"type":"string"},"loadingStrategy":{"enum":["fetch","script"],"type":"string"},"path":{"type":"string"}},"required":["path"],"type":"object"},"pluginJson":{"$ref":"#/components/schemas/JSONData"},"signature":{"additionalProperties":false,"properties":{"org":{"type":"string"},"status":{"enum":["internal","valid","invalid","modified","unsigned"],"type":"string"},"type":{"enum":["grafana","commercial","community","private","private-glob"],"type":"string"}},"required":["status"],"type":"object"},"translations":{"additionalProperties":{"type":"string"},"type":"object"}},"required":["pluginJson","class"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) versionSchemaMetav0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaMetav0alpha1, &versionSchemaMetav0alpha1) ) diff --git a/apps/plugins/pkg/app/meta/converter.go b/apps/plugins/pkg/app/meta/converter.go index b8c0c4371d7..70a1bc78b0c 100644 --- a/apps/plugins/pkg/app/meta/converter.go +++ b/apps/plugins/pkg/app/meta/converter.go @@ -565,10 +565,6 @@ func pluginStorePluginToMeta(plugin pluginstore.Plugin, loadingStrategy plugins. metaSpec.Children = plugin.Children } - metaSpec.Angular = &pluginsv0alpha1.MetaV0alpha1SpecAngular{ - Detected: plugin.Angular.Detected, - } - if len(plugin.Translations) > 0 { metaSpec.Translations = plugin.Translations } @@ -668,10 +664,6 @@ func pluginToMetaSpec(plugin *plugins.Plugin) pluginsv0alpha1.MetaSpec { metaSpec.Children = children } - metaSpec.Angular = &pluginsv0alpha1.MetaV0alpha1SpecAngular{ - Detected: plugin.Angular.Detected, - } - if len(plugin.Translations) > 0 { metaSpec.Translations = plugin.Translations } @@ -712,8 +704,7 @@ type grafanaComPluginVersionMeta struct { Rel string `json:"rel"` Href string `json:"href"` } `json:"links"` - AngularDetected bool `json:"angularDetected"` - Scopes []string `json:"scopes"` + Scopes []string `json:"scopes"` } // grafanaComPluginVersionMetaToMetaSpec converts a grafanaComPluginVersionMeta to a pluginsv0alpha1.MetaSpec. @@ -753,10 +744,5 @@ func grafanaComPluginVersionMetaToMetaSpec(gcomMeta grafanaComPluginVersionMeta) metaSpec.Signature = signature } - // Set angular info - metaSpec.Angular = &pluginsv0alpha1.MetaV0alpha1SpecAngular{ - Detected: gcomMeta.AngularDetected, - } - return metaSpec } From c9a14f177459da2375302c23032aec1d61c31916 Mon Sep 17 00:00:00 2001 From: Misi Date: Tue, 13 Jan 2026 14:45:18 +0100 Subject: [PATCH 02/19] IAM: Target resource authorization for TeamBinding (#116117) * wip * Review VerbGet vs VerbGetPermissions * Fix tests --- pkg/registry/apis/iam/authorizer.go | 2 +- .../iam/authorizer/team_binding_authorizer.go | 156 +++++++++++ .../team_binding_authorizer_test.go | 253 ++++++++++++++++++ pkg/registry/apis/iam/register.go | 12 +- .../iam/team_bindings_integration_test.go | 126 +++++---- .../testdata/teambinding-test-create-v0.yaml | 2 +- 6 files changed, 496 insertions(+), 55 deletions(-) create mode 100644 pkg/registry/apis/iam/authorizer/team_binding_authorizer.go create mode 100644 pkg/registry/apis/iam/authorizer/team_binding_authorizer_test.go diff --git a/pkg/registry/apis/iam/authorizer.go b/pkg/registry/apis/iam/authorizer.go index efcf6fa5b39..da81dd2d9a8 100644 --- a/pkg/registry/apis/iam/authorizer.go +++ b/pkg/registry/apis/iam/authorizer.go @@ -42,7 +42,6 @@ func newIAMAuthorizer( // Identity specific resources legacyAuthorizer := gfauthorizer.NewResourceAuthorizer(legacyAccessClient) - resourceAuthorizer[iamv0.TeamBindingResourceInfo.GetName()] = legacyAuthorizer resourceAuthorizer["display"] = legacyAuthorizer // Access specific resources @@ -55,6 +54,7 @@ func newIAMAuthorizer( resourceAuthorizer[iamv0.UserResourceInfo.GetName()] = authorizer resourceAuthorizer[iamv0.ExternalGroupMappingResourceInfo.GetName()] = allowAuthorizer resourceAuthorizer[iamv0.TeamResourceInfo.GetName()] = authorizer + resourceAuthorizer[iamv0.TeamBindingResourceInfo.GetName()] = allowAuthorizer resourceAuthorizer["searchUsers"] = serviceAuthorizer resourceAuthorizer["searchTeams"] = serviceAuthorizer diff --git a/pkg/registry/apis/iam/authorizer/team_binding_authorizer.go b/pkg/registry/apis/iam/authorizer/team_binding_authorizer.go new file mode 100644 index 00000000000..2a4f5ae5e51 --- /dev/null +++ b/pkg/registry/apis/iam/authorizer/team_binding_authorizer.go @@ -0,0 +1,156 @@ +package authorizer + +import ( + "context" + "fmt" + + "github.com/grafana/authlib/types" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer/storewrapper" +) + +type TeamBindingAuthorizer struct { + accessClient types.AccessClient +} + +var _ storewrapper.ResourceStorageAuthorizer = (*TeamBindingAuthorizer)(nil) + +func NewTeamBindingAuthorizer( + accessClient types.AccessClient, +) *TeamBindingAuthorizer { + return &TeamBindingAuthorizer{ + accessClient: accessClient, + } +} + +// AfterGet implements ResourceStorageAuthorizer. +func (r *TeamBindingAuthorizer) AfterGet(ctx context.Context, obj runtime.Object) error { + authInfo, ok := types.AuthInfoFrom(ctx) + if !ok { + return storewrapper.ErrUnauthenticated + } + + concreteObj, ok := obj.(*iamv0.TeamBinding) + if !ok { + return apierrors.NewInternalError(fmt.Errorf("expected TeamBinding, got %T: %w", obj, storewrapper.ErrUnexpectedType)) + } + + // Accesscontrol should check on the TeamResourceInfo group resource if the user can use VerbGetPermissions + // on the team (TeamRef.Name) (handled below) OR if the subject's name (TeamBindingSpec.Subject.Name) is equal to the current Identity's UID/Identifier. + if concreteObj.Spec.Subject.Name == authInfo.GetIdentifier() { + return nil + } + + teamName := concreteObj.Spec.TeamRef.Name + checkReq := types.CheckRequest{ + Namespace: authInfo.GetNamespace(), + Group: iamv0.TeamResourceInfo.GroupResource().Group, + Resource: iamv0.TeamResourceInfo.GroupResource().Resource, + Verb: utils.VerbGetPermissions, + Name: teamName, + } + res, err := r.accessClient.Check(ctx, authInfo, checkReq, "") + if err != nil { + return apierrors.NewInternalError(err) + } + + if !res.Allowed { + return apierrors.NewForbidden( + iamv0.TeamBindingResourceInfo.GroupResource(), + concreteObj.Name, + fmt.Errorf("user cannot access team %s", teamName), + ) + } + return nil +} + +// BeforeCreate implements ResourceStorageAuthorizer. +func (r *TeamBindingAuthorizer) BeforeCreate(ctx context.Context, obj runtime.Object) error { + return r.beforeWrite(ctx, obj) +} + +// BeforeDelete implements ResourceStorageAuthorizer. +func (r *TeamBindingAuthorizer) BeforeDelete(ctx context.Context, obj runtime.Object) error { + return r.beforeWrite(ctx, obj) +} + +// BeforeUpdate implements ResourceStorageAuthorizer. +func (r *TeamBindingAuthorizer) BeforeUpdate(ctx context.Context, obj runtime.Object) error { + return r.beforeWrite(ctx, obj) +} + +func (r *TeamBindingAuthorizer) beforeWrite(ctx context.Context, obj runtime.Object) error { + authInfo, ok := types.AuthInfoFrom(ctx) + if !ok { + return storewrapper.ErrUnauthenticated + } + + concreteObj, ok := obj.(*iamv0.TeamBinding) + if !ok { + return apierrors.NewInternalError(fmt.Errorf("expected TeamBinding, got %T: %w", obj, storewrapper.ErrUnexpectedType)) + } + + teamName := concreteObj.Spec.TeamRef.Name + checkReq := types.CheckRequest{ + Namespace: authInfo.GetNamespace(), + Group: iamv0.GROUP, + Resource: iamv0.TeamResourceInfo.GetName(), + Verb: utils.VerbSetPermissions, + Name: teamName, + } + + res, err := r.accessClient.Check(ctx, authInfo, checkReq, "") + if err != nil { + return apierrors.NewInternalError(err) + } + + if !res.Allowed { + return apierrors.NewForbidden( + iamv0.TeamBindingResourceInfo.GroupResource(), + concreteObj.Name, + fmt.Errorf("user cannot write team %s", teamName), + ) + } + return nil +} + +// FilterList implements ResourceStorageAuthorizer. +func (r *TeamBindingAuthorizer) FilterList(ctx context.Context, list runtime.Object) (runtime.Object, error) { + authInfo, ok := types.AuthInfoFrom(ctx) + if !ok { + return nil, storewrapper.ErrUnauthenticated + } + + l, ok := list.(*iamv0.TeamBindingList) + if !ok { + return nil, apierrors.NewInternalError(fmt.Errorf("expected TeamBindingList, got %T: %w", list, storewrapper.ErrUnexpectedType)) + } + + var filteredItems []iamv0.TeamBinding + + listReq := types.ListRequest{ + Namespace: authInfo.GetNamespace(), + Group: iamv0.TeamResourceInfo.GroupResource().Group, + Resource: iamv0.TeamResourceInfo.GroupResource().Resource, + Verb: utils.VerbGetPermissions, + } + canView, _, err := r.accessClient.Compile(ctx, authInfo, listReq) + if err != nil { + return nil, apierrors.NewInternalError(err) + } + + for _, item := range l.Items { + // Accesscontrol should check on the TeamResourceInfo group resource if the user can use VerbGetPermissions + // on the team (TeamRef.Name) OR if the subject's name (TeamBindingSpec.Subject.Name) is equal to the current Identity's UID/Identifier. + if item.Spec.Subject.Name == authInfo.GetIdentifier() || canView(item.Spec.TeamRef.Name, "") { + filteredItems = append(filteredItems, item) + } + } + + l.Items = filteredItems + return l, nil +} diff --git a/pkg/registry/apis/iam/authorizer/team_binding_authorizer_test.go b/pkg/registry/apis/iam/authorizer/team_binding_authorizer_test.go new file mode 100644 index 00000000000..f3bf8795de1 --- /dev/null +++ b/pkg/registry/apis/iam/authorizer/team_binding_authorizer_test.go @@ -0,0 +1,253 @@ +package authorizer + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/grafana/authlib/types" + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" +) + +func newTeamBinding(teamName, name, subjectName string) *iamv0.TeamBinding { + return &iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{Namespace: "org-2", Name: name}, + Spec: iamv0.TeamBindingSpec{ + TeamRef: iamv0.TeamBindingTeamRef{ + Name: teamName, + }, + Subject: iamv0.TeamBindingspecSubject{ + Name: subjectName, + }, + }, + } +} + +func TestTeamBinding_AfterGet(t *testing.T) { + tests := []struct { + name string + teamBinding *iamv0.TeamBinding + shouldAllow bool + checkCalled bool + }{ + { + name: "allow access via permission", + teamBinding: newTeamBinding("team-1", "binding-1", "other"), + shouldAllow: true, + checkCalled: true, + }, + { + name: "deny access", + teamBinding: newTeamBinding("team-1", "binding-1", "other"), + shouldAllow: false, + checkCalled: true, // called but returns allowed=false + }, + { + name: "allow access via subject match", + teamBinding: newTeamBinding("team-1", "binding-1", "u001"), + shouldAllow: true, + checkCalled: false, // short-circuits + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) { + require.NotNil(t, id) + require.Equal(t, "u001", id.GetIdentifier()) + + require.Equal(t, "org-2", req.Namespace) + require.Equal(t, iamv0.GROUP, req.Group) + require.Equal(t, iamv0.TeamResourceInfo.GetName(), req.Resource) + require.Equal(t, "team-1", req.Name) + require.Equal(t, utils.VerbGetPermissions, req.Verb) + + return types.CheckResponse{Allowed: tt.shouldAllow}, nil + } + + accessClient := &fakeAccessClient{checkFunc: checkFunc} + authz := NewTeamBindingAuthorizer(accessClient) + ctx := types.WithAuthInfo(context.Background(), user) + + err := authz.AfterGet(ctx, tt.teamBinding) + if tt.shouldAllow { + require.NoError(t, err) + } else { + require.Error(t, err) + } + require.Equal(t, tt.checkCalled, accessClient.checkCalled) + }) + } +} + +func TestTeamBinding_FilterList(t *testing.T) { + list := &iamv0.TeamBindingList{ + Items: []iamv0.TeamBinding{ + *newTeamBinding("team-1", "binding-1", "other"), // Access via permission + *newTeamBinding("team-2", "binding-2", "other"), // No access + *newTeamBinding("team-3", "binding-3", "u001"), // Access via subject match + }, + } + + compileFunc := func(id types.AuthInfo, req types.ListRequest) (types.ItemChecker, types.Zookie, error) { + require.NotNil(t, id) + require.Equal(t, "u001", id.GetIdentifier()) + + require.Equal(t, "org-2", req.Namespace) + require.Equal(t, iamv0.GROUP, req.Group) + require.Equal(t, iamv0.TeamResourceInfo.GroupResource().Resource, req.Resource) + require.Equal(t, utils.VerbGetPermissions, req.Verb) + + return func(name, folder string) bool { + return name == "team-1" + }, &types.NoopZookie{}, nil + } + + accessClient := &fakeAccessClient{compileFunc: compileFunc} + authz := NewTeamBindingAuthorizer(accessClient) + ctx := types.WithAuthInfo(context.Background(), user) + + obj, err := authz.FilterList(ctx, list) + require.NoError(t, err) + require.NotNil(t, list) + require.True(t, accessClient.compileCalled) + + filtered, ok := obj.(*iamv0.TeamBindingList) + require.True(t, ok) + require.Len(t, filtered.Items, 2) + + names := []string{filtered.Items[0].Name, filtered.Items[1].Name} + require.Contains(t, names, "binding-1") + require.Contains(t, names, "binding-3") +} + +func TestTeamBinding_BeforeCreate(t *testing.T) { + binding := newTeamBinding("team-1", "binding-1", "other") + + tests := []struct { + name string + shouldAllow bool + }{ + { + name: "allow create", + shouldAllow: true, + }, + { + name: "deny create", + shouldAllow: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) { + require.Equal(t, "org-2", req.Namespace) + require.Equal(t, iamv0.GROUP, req.Group) + require.Equal(t, iamv0.TeamResourceInfo.GetName(), req.Resource) + require.Equal(t, "team-1", req.Name) + require.Equal(t, utils.VerbSetPermissions, req.Verb) + + return types.CheckResponse{Allowed: tt.shouldAllow}, nil + } + + accessClient := &fakeAccessClient{checkFunc: checkFunc} + authz := NewTeamBindingAuthorizer(accessClient) + ctx := types.WithAuthInfo(context.Background(), user) + + err := authz.BeforeCreate(ctx, binding) + if tt.shouldAllow { + require.NoError(t, err) + } else { + require.Error(t, err) + } + require.True(t, accessClient.checkCalled) + }) + } +} + +func TestTeamBinding_BeforeUpdate(t *testing.T) { + binding := newTeamBinding("team-1", "binding-1", "other") + + tests := []struct { + name string + shouldAllow bool + }{ + { + name: "allow update", + shouldAllow: true, + }, + { + name: "deny update", + shouldAllow: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) { + require.Equal(t, "org-2", req.Namespace) + require.Equal(t, iamv0.GROUP, req.Group) + require.Equal(t, iamv0.TeamResourceInfo.GetName(), req.Resource) + require.Equal(t, "team-1", req.Name) + require.Equal(t, utils.VerbSetPermissions, req.Verb) + + return types.CheckResponse{Allowed: tt.shouldAllow}, nil + } + + accessClient := &fakeAccessClient{checkFunc: checkFunc} + authz := NewTeamBindingAuthorizer(accessClient) + ctx := types.WithAuthInfo(context.Background(), user) + + err := authz.BeforeUpdate(ctx, binding) + if tt.shouldAllow { + require.NoError(t, err) + } else { + require.Error(t, err) + } + require.True(t, accessClient.checkCalled) + }) + } +} + +func TestTeamBinding_BeforeDelete(t *testing.T) { + binding := newTeamBinding("team-1", "binding-1", "other") + + tests := []struct { + name string + shouldAllow bool + }{ + { + name: "allow delete", + shouldAllow: true, + }, + { + name: "deny delete", + shouldAllow: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) { + require.Equal(t, "org-2", req.Namespace) + require.Equal(t, iamv0.GROUP, req.Group) + require.Equal(t, iamv0.TeamResourceInfo.GetName(), req.Resource) + require.Equal(t, "team-1", req.Name) + require.Equal(t, utils.VerbSetPermissions, req.Verb) + + return types.CheckResponse{Allowed: tt.shouldAllow}, nil + } + + accessClient := &fakeAccessClient{checkFunc: checkFunc} + authz := NewTeamBindingAuthorizer(accessClient) + ctx := types.WithAuthInfo(context.Background(), user) + + err := authz.BeforeDelete(ctx, binding) + if tt.shouldAllow { + require.NoError(t, err) + } else { + require.Error(t, err) + } + require.True(t, accessClient.checkCalled) + }) + } +} diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 7f42d620987..895f1d0bf22 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -361,7 +361,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateTeamBindingsAPIGroup(opts bui if err != nil { return err } - storage[teamBindingResource.StoragePath()] = teamBindingUniStore + var teamBindingStore storewrapper.K8sStorage = teamBindingUniStore // Only teamBindingStore exposes the AfterCreate, AfterDelete, and BeginUpdate hooks if enableZanzanaSync { @@ -376,8 +376,16 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateTeamBindingsAPIGroup(opts bui if err != nil { return err } - storage[teamBindingResource.StoragePath()] = dw + + var ok bool + teamBindingStore, ok = dw.(storewrapper.K8sStorage) + if !ok { + return fmt.Errorf("expected storewrapper.K8sStorage, got %T", dw) + } } + + authzWrapper := storewrapper.New(teamBindingStore, iamauthorizer.NewTeamBindingAuthorizer(b.accessClient)) + storage[teamBindingResource.StoragePath()] = authzWrapper return nil } diff --git a/pkg/tests/apis/iam/team_bindings_integration_test.go b/pkg/tests/apis/iam/team_bindings_integration_test.go index 1b355296486..40258edaf45 100644 --- a/pkg/tests/apis/iam/team_bindings_integration_test.go +++ b/pkg/tests/apis/iam/team_bindings_integration_test.go @@ -67,7 +67,7 @@ func TestIntegrationTeamBindings(t *testing.T) { doTeamBindingCRUDTestsUsingTheNewAPIs(t, helper, team, user) if mode < 3 { - doTeamBindingCRUDTestsUsingTheLegacyAPIs(t, helper, mode) + doTeamBindingCRUDTestsUsingTheLegacyAPIs(t, helper) } }) } @@ -84,13 +84,15 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel }) // Create the team binding - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() + toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName()) created, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) require.NoError(t, err) require.NotNil(t, created) + defer func() { + _ = teamBindingClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{}) + }() + createdSpec := created.Object["spec"].(map[string]interface{}) require.Equal(t, user.GetName(), createdSpec["subject"].(map[string]interface{})["name"]) require.Equal(t, team.GetName(), createdSpec["teamRef"].(map[string]interface{})["name"]) @@ -115,6 +117,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel // Update the team binding toUpdate := toCreate.DeepCopy() toUpdate.Object["spec"].(map[string]interface{})["permission"] = "member" + toUpdate.Object["metadata"].(map[string]interface{})["name"] = createdUID updated, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) require.NoError(t, err) require.NotNil(t, updated) @@ -164,9 +167,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() + toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName()) _, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) require.Error(t, err) @@ -185,9 +186,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = "" - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() + toCreate := createTeamBindingObject(helper, "", team.GetName()) _, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) require.Error(t, err) @@ -205,9 +204,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = "" + toCreate := createTeamBindingObject(helper, user.GetName(), "") _, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) require.Error(t, err) @@ -225,9 +222,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() + toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName()) toCreate.Object["spec"].(map[string]interface{})["permission"] = "invalid" _, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) @@ -245,17 +240,31 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel } { t.Run(fmt.Sprintf("with basic role_%s", u.Identity.GetOrgRole()), func(t *testing.T) { ctx := context.Background() + + // Create the team binding using admin + adminClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), + GVR: gvrTeamBindings, + }) + + toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName()) + created, err := adminClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) + + defer func() { + _ = adminClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{}) + }() + teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: u, Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), GVR: gvrTeamBindings, }) - toUpdate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toUpdate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toUpdate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() + toUpdate := created.DeepCopy() toUpdate.Object["spec"].(map[string]interface{})["permission"] = "member" - _, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) + _, err = teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) require.Error(t, err) var statusErr *errors.StatusError @@ -273,10 +282,8 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - toUpdate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") + toUpdate := createTeamBindingObject(helper, user.GetName(), team.GetName()) toUpdate.Object["metadata"].(map[string]interface{})["name"] = "invalid-team-binding-name" - toUpdate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toUpdate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() _, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) require.Error(t, err) var statusErr *errors.StatusError @@ -293,15 +300,18 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - // Create the team binding if it doesn't already exist - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() - _, _ = teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName()) + created, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) + + defer func() { + _ = teamBindingClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{}) + }() toUpdate := toCreate.DeepCopy() toUpdate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = "test-team-2" - _, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) + toUpdate.Object["metadata"].(map[string]interface{})["name"] = created.GetName() + _, err = teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) require.Error(t, err) var statusErr *errors.StatusError require.ErrorAs(t, err, &statusErr) @@ -317,16 +327,19 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - // Create the team binding if it doesn't already exist - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() - _, _ = teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName()) + created, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) + + defer func() { + _ = teamBindingClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{}) + }() toUpdate := toCreate.DeepCopy() + toUpdate.Object["metadata"].(map[string]interface{})["name"] = created.GetName() toUpdate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = "test-user-2" - _, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) + _, err = teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) require.Error(t, err) var statusErr *errors.StatusError require.ErrorAs(t, err, &statusErr) @@ -342,15 +355,18 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - // Create the team binding if it doesn't already exist - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() - _, _ = teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName()) + created, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) + + defer func() { + _ = teamBindingClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{}) + }() toUpdate := toCreate.DeepCopy() toUpdate.Object["spec"].(map[string]interface{})["external"] = true - _, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) + toUpdate.Object["metadata"].(map[string]interface{})["name"] = created.GetName() + _, err = teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) require.Error(t, err) var statusErr *errors.StatusError require.ErrorAs(t, err, &statusErr) @@ -366,17 +382,18 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - // Create the team binding if it doesn't already exist - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() - _, _ = teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName()) + created, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) - toUpdate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toUpdate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toUpdate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() + defer func() { + _ = teamBindingClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{}) + }() + + toUpdate := createTeamBindingObject(helper, user.GetName(), team.GetName()) toUpdate.Object["spec"].(map[string]interface{})["permission"] = "invalid" - _, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) + toUpdate.Object["metadata"].(map[string]interface{})["name"] = created.GetName() + _, err = teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) require.Error(t, err) var statusErr *errors.StatusError require.ErrorAs(t, err, &statusErr) @@ -385,7 +402,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel }) } -func doTeamBindingCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper, mode rest.DualWriterMode) { +func doTeamBindingCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper) { t.Run("should create team binding using legacy APIs and get it using the new APIs", func(t *testing.T) { ctx := context.Background() @@ -499,3 +516,10 @@ func doTeamBindingCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTest require.Equal(t, teamBindingName, teamBinding.GetName()) }) } + +func createTeamBindingObject(helper *apis.K8sTestHelper, userName, teamName string) *unstructured.Unstructured { + obj := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") + obj.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = userName + obj.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = teamName + return obj +} diff --git a/pkg/tests/apis/iam/testdata/teambinding-test-create-v0.yaml b/pkg/tests/apis/iam/testdata/teambinding-test-create-v0.yaml index da04e7785b1..2ac36c1b6a6 100644 --- a/pkg/tests/apis/iam/testdata/teambinding-test-create-v0.yaml +++ b/pkg/tests/apis/iam/testdata/teambinding-test-create-v0.yaml @@ -1,7 +1,7 @@ apiVersion: iam.grafana.app/v0alpha1 kind: TeamBinding metadata: - name: test-team-binding-1 + generateName: test-team-binding- spec: subject: name: "" From 86652a6515ce0c5d19e646b18673cf31ac85c20e Mon Sep 17 00:00:00 2001 From: Will Assis <35489495+gassiss@users.noreply.github.com> Date: Tue, 13 Jan 2026 09:35:02 -0500 Subject: [PATCH 03/19] unified-storage: sql backend key_path backfill (#116142) --- .../unified/sql/db/migrations/resource_mig.go | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/pkg/storage/unified/sql/db/migrations/resource_mig.go b/pkg/storage/unified/sql/db/migrations/resource_mig.go index 8f9be689718..8f7abe1306e 100644 --- a/pkg/storage/unified/sql/db/migrations/resource_mig.go +++ b/pkg/storage/unified/sql/db/migrations/resource_mig.go @@ -2,8 +2,11 @@ package migrations import ( "fmt" + "strings" + "github.com/bwmarrin/snowflake" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/util/xorm" ) func initResourceTables(mg *migrator.Migrator) string { @@ -220,5 +223,142 @@ func initResourceTables(mg *migrator.Migrator) string { mg.AddMigration("Change key_path collation of resource_history in postgres", migrator.NewRawSQLMigration("").Postgres(`ALTER TABLE resource_history ALTER COLUMN key_path TYPE VARCHAR(2048) COLLATE "C";`)) mg.AddMigration("Change key_path collation of resource_events in postgres", migrator.NewRawSQLMigration("").Postgres(`ALTER TABLE resource_events ALTER COLUMN key_path TYPE VARCHAR(2048) COLLATE "C";`)) + mg.AddMigration("resource_history key_path backfill", &ResourceHistoryKeyPathBackfillMigration{}) + return marker } + +type ResourceHistoryKeyPathBackfillMigration struct { + migrator.MigrationBase +} + +func (m *ResourceHistoryKeyPathBackfillMigration) SQL(_ migrator.Dialect) string { + return "resource_history key_path backfill code migration" +} + +func (m *ResourceHistoryKeyPathBackfillMigration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { + rows, err := getResourceHistoryRows(sess, mg, resourceHistoryRow{}) + if err != nil { + return err + } + + for len(rows) > 0 { + if err := updateResourceHistoryKeyPath(sess, rows); err != nil { + return err + } + + rows, err = getResourceHistoryRows(sess, mg, rows[len(rows)-1]) + if err != nil { + return err + } + } + + return nil +} + +func updateResourceHistoryKeyPath(sess *xorm.Session, rows []resourceHistoryRow) error { + if len(rows) == 0 { + return nil + } + + updates := []resourceHistoryRow{} + + for _, row := range rows { + if row.KeyPath == "" { + row.KeyPath = parseKeyPath(row) + updates = append(updates, row) + } + } + + if len(updates) == 0 { + return nil + } + + guids := "" + setCases := "CASE" + for _, row := range updates { + guids += fmt.Sprintf("'%s',", row.GUID) + setCases += fmt.Sprintf(" WHEN guid = '%s' THEN '%s'", row.GUID, row.KeyPath) + } + + guids = strings.TrimRight(guids, ",") + setCases += " ELSE key_path END " + + // the query will look like this + // UPDATE resource_history + // SET key_path = CASE + // WHEN guid = '1402de51-669b-4206-8a6c-005a00eee6e3' then 'unified/data/folder.grafana.app/folders/default/cf6lylpvls000c/1998492888241012800~created~' + // WHEN guid = '8842cc56-f22b-45e1-82b1-99759cd443b3' then 'unified/data/dashboard.grafana.app/dashboards/default/adzvfhp/1998492902577144677~created~cf6lylpvls000c' + // ELSE key_path END + // WHERE guid IN ('1402de51-669b-4206-8a6c-005a00eee6e3', '8842cc56-f22b-45e1-82b1-99759cd443b3') + // AND key_path = ''; + sql := fmt.Sprintf(` + UPDATE resource_history + SET key_path = %s + WHERE guid IN (%s) + AND key_path = ''; + `, setCases, guids) + + if _, err := sess.Exec(sql); err != nil { + return err + } + + return nil +} + +func parseKeyPath(row resourceHistoryRow) string { + var action string + switch row.Action { + case 1: + action = "created" + case 2: + action = "updated" + case 3: + action = "deleted" + } + return fmt.Sprintf("unified/data/%s/%s/%s/%s/%d~%s~%s", row.Group, row.Resource, row.Namespace, row.Name, snowflakeFromRv(row.ResourceVersion), action, row.Folder) +} + +func snowflakeFromRv(rv int64) int64 { + return (((rv / 1000) - snowflake.Epoch) << (snowflake.NodeBits + snowflake.StepBits)) + (rv % 1000) +} + +type resourceHistoryRow struct { + GUID string `xorm:"guid"` + Group string `xorm:"group"` + Resource string `xorm:"resource"` + Namespace string `xorm:"namespace"` + Name string `xorm:"name"` + ResourceVersion int64 `xorm:"resource_version"` + Action int64 `xorm:"action"` + Folder string `xorm:"folder"` + KeyPath string `xorm:"key_path"` +} + +func getResourceHistoryRows(sess *xorm.Session, mg *migrator.Migrator, continueRow resourceHistoryRow) ([]resourceHistoryRow, error) { + var rows []resourceHistoryRow + cols := fmt.Sprintf( + "%s, %s, %s, %s, %s, %s, %s, %s, %s", + mg.Dialect.Quote("guid"), + mg.Dialect.Quote("group"), + mg.Dialect.Quote("resource"), + mg.Dialect.Quote("namespace"), + mg.Dialect.Quote("name"), + mg.Dialect.Quote("resource_version"), + mg.Dialect.Quote("action"), + mg.Dialect.Quote("folder"), + mg.Dialect.Quote("key_path")) + sql := fmt.Sprintf(` + SELECT %s + FROM resource_history + WHERE (resource_version > %d OR (resource_version = %d AND guid > '%s')) + AND key_path = '' + ORDER BY resource_version ASC, guid ASC + LIMIT 1000; + `, cols, continueRow.ResourceVersion, continueRow.ResourceVersion, continueRow.GUID) + if err := sess.SQL(sql).Find(&rows); err != nil { + return nil, err + } + + return rows, nil +} From aa9b587cc1e50bcbfe31d78e98473c78d451bf67 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Tue, 13 Jan 2026 14:35:45 +0000 Subject: [PATCH 04/19] Plugins: Add module hash field to plugin model (#116119) * add module hash field to plugin model * fix tests * fix lint issues --- apps/plugins/pkg/app/meta/local.go | 7 +- pkg/api/frontendsettings.go | 6 +- pkg/api/frontendsettings_test.go | 7 +- pkg/api/plugins.go | 2 +- pkg/api/plugins_test.go | 5 +- pkg/plugins/ifaces.go | 4 +- pkg/plugins/manager/loader/loader_test.go | 25 +- .../manager/pipeline/bootstrap/bootstrap.go | 3 +- .../manager/pipeline/bootstrap/steps.go | 27 +- pkg/plugins/manager/signature/manifest.go | 55 +-- .../manager/signature/manifest_test.go | 32 +- pkg/plugins/pluginassets/modulehash.go | 90 +++++ pkg/plugins/pluginassets/modulehash_test.go | 356 +++++++++++++++++ .../module-hash-no-manifest-txt/module.js | 0 .../module-hash-no-manifest-txt/plugin.json | 0 .../module-hash-no-module-js/MANIFEST.txt | 0 .../module-hash-no-module-js/plugin.json | 0 .../module-hash-no-module-js/something.js | 0 .../MANIFEST.txt | 0 .../datasource/module.js | 0 .../datasource/panels/one/module.js | 0 .../datasource/panels/one/plugin.json | 0 .../datasource/plugin.json | 0 .../module-hash-valid-deeply-nested/module.js | 0 .../plugin.json | 0 .../module-hash-valid-nested/MANIFEST.txt | 0 .../datasource/module.js | 0 .../datasource/plugin.json | 0 .../module-hash-valid-nested/module.js | 0 .../panels/one/module.js | 0 .../panels/one/plugin.json | 0 .../module-hash-valid-nested/plugin.json | 0 .../testdata/module-hash-valid/MANIFEST.txt | 0 .../testdata/module-hash-valid/module.js | 0 .../testdata/module-hash-valid/plugin.json | 0 pkg/plugins/plugins.go | 27 +- pkg/server/wire_gen.go | 12 +- .../pluginsintegration/loader/loader_test.go | 206 ++++++++-- .../pluginsintegration/pipeline/pipeline.go | 5 +- .../pluginassets/pluginassets.go | 132 +----- .../pluginassets/pluginassets_test.go | 375 ------------------ .../pluginsintegration/pluginstore/plugins.go | 6 +- .../pluginsintegration/test_helper.go | 5 +- 43 files changed, 767 insertions(+), 620 deletions(-) create mode 100644 pkg/plugins/pluginassets/modulehash.go create mode 100644 pkg/plugins/pluginassets/modulehash_test.go rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-no-manifest-txt/module.js (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-no-manifest-txt/plugin.json (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-no-module-js/MANIFEST.txt (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-no-module-js/plugin.json (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-no-module-js/something.js (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-valid-deeply-nested/MANIFEST.txt (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/module.js (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/module.js (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/plugin.json (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/plugin.json (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-valid-deeply-nested/module.js (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-valid-deeply-nested/plugin.json (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-valid-nested/MANIFEST.txt (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-valid-nested/datasource/module.js (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-valid-nested/datasource/plugin.json (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-valid-nested/module.js (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-valid-nested/panels/one/module.js (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-valid-nested/panels/one/plugin.json (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-valid-nested/plugin.json (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-valid/MANIFEST.txt (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-valid/module.js (100%) rename pkg/{services/pluginsintegration => plugins}/pluginassets/testdata/module-hash-valid/plugin.json (100%) diff --git a/apps/plugins/pkg/app/meta/local.go b/apps/plugins/pkg/app/meta/local.go index 2c699520cfc..af40316cd73 100644 --- a/apps/plugins/pkg/app/meta/local.go +++ b/apps/plugins/pkg/app/meta/local.go @@ -13,10 +13,9 @@ const ( ) // PluginAssetsCalculator is an interface for calculating plugin asset information. -// LocalProvider requires this to calculate loading strategy and module hash. +// LocalProvider requires this to calculate loading strategy. type PluginAssetsCalculator interface { LoadingStrategy(ctx context.Context, p pluginstore.Plugin) plugins.LoadingStrategy - ModuleHash(ctx context.Context, p pluginstore.Plugin) string } // LocalProvider retrieves plugin metadata for locally installed plugins. @@ -27,7 +26,7 @@ type LocalProvider struct { } // NewLocalProvider creates a new LocalProvider for locally installed plugins. -// pluginAssets is required for calculating loading strategy and module hash. +// pluginAssets is required for calculating loading strategy. func NewLocalProvider(pluginStore pluginstore.Store, pluginAssets PluginAssetsCalculator) *LocalProvider { return &LocalProvider{ store: pluginStore, @@ -43,7 +42,7 @@ func (p *LocalProvider) GetMeta(ctx context.Context, pluginID, version string) ( } loadingStrategy := p.pluginAssets.LoadingStrategy(ctx, plugin) - moduleHash := p.pluginAssets.ModuleHash(ctx, plugin) + moduleHash := plugin.ModuleHash spec := pluginStorePluginToMeta(plugin, loadingStrategy, moduleHash) return &Result{ diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index b57262087e5..64165cfb98a 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -161,7 +161,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro AliasIDs: panel.AliasIDs, Info: panel.Info, Module: panel.Module, - ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), panel), + ModuleHash: panel.ModuleHash, BaseURL: panel.BaseURL, SkipDataQuery: panel.SkipDataQuery, Suggestions: panel.Suggestions, @@ -527,7 +527,7 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug JSONData: plugin.JSONData, Signature: plugin.Signature, Module: plugin.Module, - ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), plugin), + ModuleHash: plugin.ModuleHash, BaseURL: plugin.BaseURL, Angular: plugin.Angular, MultiValueFilterOperators: plugin.MultiValueFilterOperators, @@ -641,7 +641,7 @@ func (hs *HTTPServer) newAppDTO(ctx context.Context, plugin pluginstore.Plugin, LoadingStrategy: hs.pluginAssets.LoadingStrategy(ctx, plugin), Extensions: plugin.Extensions, Dependencies: plugin.Dependencies, - ModuleHash: hs.pluginAssets.ModuleHash(ctx, plugin), + ModuleHash: plugin.ModuleHash, Translations: plugin.Translations, BuildMode: plugin.BuildMode, } diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index ba67837be5d..4741f6b10bc 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -20,8 +20,6 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" - "github.com/grafana/grafana/pkg/plugins/manager/signature" - "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/pluginscdn" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -79,8 +77,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F var pluginsAssets = passets if pluginsAssets == nil { - sig := signature.ProvideService(pluginsCfg, statickey.New()) - pluginsAssets = pluginassets.ProvideService(pluginsCfg, pluginsCDN, sig, pluginStore) + pluginsAssets = pluginassets.ProvideService(pluginsCfg, pluginsCDN, pluginStore) } hs := &HTTPServer{ @@ -714,6 +711,6 @@ func newPluginAssets() func() *pluginassets.Service { func newPluginAssetsWithConfig(pCfg *config.PluginManagementCfg) func() *pluginassets.Service { return func() *pluginassets.Service { - return pluginassets.ProvideService(pCfg, pluginscdn.ProvideService(pCfg), signature.ProvideService(pCfg, statickey.New()), &pluginstore.FakePluginStore{}) + return pluginassets.ProvideService(pCfg, pluginscdn.ProvideService(pCfg), &pluginstore.FakePluginStore{}) } } diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 50e32326565..079dae066bb 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -201,7 +201,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response. Includes: plugin.Includes, BaseUrl: plugin.BaseURL, Module: plugin.Module, - ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), plugin), + ModuleHash: plugin.ModuleHash, DefaultNavUrl: path.Join(hs.Cfg.AppSubURL, plugin.DefaultNavURL), State: plugin.State, Signature: plugin.Signature, diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index 342c6293d7e..238b0b3390f 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -28,8 +28,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/filestore" "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" "github.com/grafana/grafana/pkg/plugins/manager/registry" - "github.com/grafana/grafana/pkg/plugins/manager/signature" - "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/pluginerrs" "github.com/grafana/grafana/pkg/plugins/pluginscdn" ac "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -848,8 +846,7 @@ func Test_PluginsSettings(t *testing.T) { } pCfg := &config.PluginManagementCfg{} pluginCDN := pluginscdn.ProvideService(pCfg) - sig := signature.ProvideService(pCfg, statickey.New()) - hs.pluginAssets = pluginassets.ProvideService(pCfg, pluginCDN, sig, hs.pluginStore) + hs.pluginAssets = pluginassets.ProvideService(pCfg, pluginCDN, hs.pluginStore) hs.pluginErrorResolver = pluginerrs.ProvideStore(errTracker) hs.pluginsUpdateChecker, err = updatemanager.ProvidePluginsService( hs.Cfg, diff --git a/pkg/plugins/ifaces.go b/pkg/plugins/ifaces.go index 9b719b8b180..1d33d34e6c9 100644 --- a/pkg/plugins/ifaces.go +++ b/pkg/plugins/ifaces.go @@ -140,7 +140,9 @@ type Licensing interface { } type SignatureCalculator interface { - Calculate(ctx context.Context, src PluginSource, plugin FoundPlugin) (Signature, error) + // Calculate calculates the signature and returns both the signature and the manifest. + // The manifest may be nil if the plugin is unsigned or if an error occurred. + Calculate(ctx context.Context, src PluginSource, plugin FoundPlugin) (Signature, *PluginManifest, error) } type KeyStore interface { diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 7613392f497..f4c63e71e15 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -216,10 +216,27 @@ func TestLoader_Load(t *testing.T) { ExtensionPoints: []plugins.ExtensionPoint{}, }, }, - Class: plugins.ClassExternal, - Module: "public/plugins/test-app/module.js", - BaseURL: "public/plugins/test-app", - FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/includes-symlinks")), + Class: plugins.ClassExternal, + Module: "public/plugins/test-app/module.js", + BaseURL: "public/plugins/test-app", + FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/includes-symlinks")), + Manifest: &plugins.PluginManifest{ + Plugin: "test-app", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1622547655175, + Files: map[string]string{ + "dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303", + "dashboards/extra/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee", + "plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf", + "symlink_to_txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048", + "text.txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypeGrafana, + SignedByOrg: "grafana", + SignedByOrgName: "Grafana Labs", + }, Signature: "valid", SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", diff --git a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go index f20c1ff1ead..c129e203dc5 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go +++ b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/pluginassets" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/tracing" "github.com/grafana/grafana/pkg/semconv" ) @@ -54,7 +55,7 @@ func New(cfg *config.PluginManagementCfg, opts Opts) *Bootstrap { } if opts.DecorateFuncs == nil { - opts.DecorateFuncs = DefaultDecorateFuncs(cfg) + opts.DecorateFuncs = DefaultDecorateFuncs(cfg, pluginscdn.ProvideService(cfg)) } return &Bootstrap{ diff --git a/pkg/plugins/manager/pipeline/bootstrap/steps.go b/pkg/plugins/manager/pipeline/bootstrap/steps.go index 5c365ebb47c..5ab85dc6e43 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/steps.go +++ b/pkg/plugins/manager/pipeline/bootstrap/steps.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/pluginassets" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" ) // DefaultConstructor implements the default ConstructFunc used for the Construct step of the Bootstrap stage. @@ -28,12 +29,13 @@ func DefaultConstructFunc(cfg *config.PluginManagementCfg, signatureCalculator p } // DefaultDecorateFuncs are the default DecorateFuncs used for the Decorate step of the Bootstrap stage. -func DefaultDecorateFuncs(cfg *config.PluginManagementCfg) []DecorateFunc { +func DefaultDecorateFuncs(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service) []DecorateFunc { return []DecorateFunc{ AppDefaultNavURLDecorateFunc, TemplateDecorateFunc, AppChildDecorateFunc(), SkipHostEnvVarsDecorateFunc(cfg), + ModuleHashDecorateFunc(cfg, cdn), } } @@ -48,19 +50,30 @@ func NewDefaultConstructor(cfg *config.PluginManagementCfg, signatureCalculator // Construct will calculate the plugin's signature state and create the plugin using the pluginFactoryFunc. func (c *DefaultConstructor) Construct(ctx context.Context, src plugins.PluginSource, bundle *plugins.FoundBundle) ([]*plugins.Plugin, error) { - sig, err := c.signatureCalculator.Calculate(ctx, src, bundle.Primary) + // Calculate signature and cache manifest + sig, manifest, err := c.signatureCalculator.Calculate(ctx, src, bundle.Primary) if err != nil { c.log.Warn("Could not calculate plugin signature state", "pluginId", bundle.Primary.JSONData.ID, "error", err) return nil, err } + plugin, err := c.pluginFactoryFunc(bundle, src.PluginClass(ctx), sig) if err != nil { c.log.Error("Could not create primary plugin base", "pluginId", bundle.Primary.JSONData.ID, "error", err) return nil, err } + + plugin.Manifest = manifest + res := make([]*plugins.Plugin, 0, len(plugin.Children)+1) res = append(res, plugin) - res = append(res, plugin.Children...) + for _, child := range plugin.Children { + // Child plugins use the parent's manifest + if child.Parent != nil && child.Parent.Manifest != nil { + child.Manifest = child.Parent.Manifest + } + res = append(res, child) + } return res, nil } @@ -145,3 +158,11 @@ func SkipHostEnvVarsDecorateFunc(cfg *config.PluginManagementCfg) DecorateFunc { return p, nil } } + +// ModuleHashDecorateFunc returns a DecorateFunc that calculates and sets the module hash for the plugin. +func ModuleHashDecorateFunc(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service) DecorateFunc { + return func(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { + p.ModuleHash = pluginassets.CalculateModuleHash(p, cfg, cdn) + return p, nil + } +} diff --git a/pkg/plugins/manager/signature/manifest.go b/pkg/plugins/manager/signature/manifest.go index 6d790c79873..4b6bc531d89 100644 --- a/pkg/plugins/manager/signature/manifest.go +++ b/pkg/plugins/manager/signature/manifest.go @@ -14,7 +14,6 @@ import ( "path" "path/filepath" "runtime" - "strings" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/clearsign" @@ -37,26 +36,6 @@ var ( fromSlash = filepath.FromSlash ) -// PluginManifest holds details for the file manifest -type PluginManifest struct { - Plugin string `json:"plugin"` - Version string `json:"version"` - KeyID string `json:"keyId"` - Time int64 `json:"time"` - Files map[string]string `json:"files"` - - // V2 supported fields - ManifestVersion string `json:"manifestVersion"` - SignatureType plugins.SignatureType `json:"signatureType"` - SignedByOrg string `json:"signedByOrg"` - SignedByOrgName string `json:"signedByOrgName"` - RootURLs []string `json:"rootUrls"` -} - -func (m *PluginManifest) IsV2() bool { - return strings.HasPrefix(m.ManifestVersion, "2.") -} - type Signature struct { kr plugins.KeyRetriever cfg *config.PluginManagementCfg @@ -87,14 +66,14 @@ func DefaultCalculator(cfg *config.PluginManagementCfg) *Signature { // readPluginManifest attempts to read and verify the plugin manifest // if any error occurs or the manifest is not valid, this will return an error -func (s *Signature) readPluginManifest(ctx context.Context, body []byte) (*PluginManifest, error) { +func (s *Signature) readPluginManifest(ctx context.Context, body []byte) (*plugins.PluginManifest, error) { block, _ := clearsign.Decode(body) if block == nil { return nil, errors.New("unable to decode manifest") } // Convert to a well typed object - var manifest PluginManifest + var manifest plugins.PluginManifest err := json.Unmarshal(block.Plaintext, &manifest) if err != nil { return nil, fmt.Errorf("%v: %w", "Error parsing manifest JSON", err) @@ -111,7 +90,7 @@ var ErrSignatureTypeUnsigned = errors.New("plugin is unsigned") // ReadPluginManifestFromFS reads the plugin manifest from the provided plugins.FS. // If the manifest is not found, it will return an error wrapping ErrSignatureTypeUnsigned. -func (s *Signature) ReadPluginManifestFromFS(ctx context.Context, pfs plugins.FS) (*PluginManifest, error) { +func (s *Signature) ReadPluginManifestFromFS(ctx context.Context, pfs plugins.FS) (*plugins.PluginManifest, error) { f, err := pfs.Open("MANIFEST.txt") if err != nil { if errors.Is(err, plugins.ErrFileNotExist) { @@ -140,9 +119,9 @@ func (s *Signature) ReadPluginManifestFromFS(ctx context.Context, pfs plugins.FS return manifest, nil } -func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plugin plugins.FoundPlugin) (plugins.Signature, error) { +func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plugin plugins.FoundPlugin) (plugins.Signature, *plugins.PluginManifest, error) { if defaultSignature, exists := src.DefaultSignature(ctx, plugin.JSONData.ID); exists { - return defaultSignature, nil + return defaultSignature, nil, nil } manifest, err := s.ReadPluginManifestFromFS(ctx, plugin.FS) @@ -151,29 +130,29 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu s.log.Warn("Plugin is unsigned", "id", plugin.JSONData.ID, "err", err) return plugins.Signature{ Status: plugins.SignatureStatusUnsigned, - }, nil + }, nil, nil case err != nil: s.log.Warn("Plugin signature is invalid", "id", plugin.JSONData.ID, "err", err) return plugins.Signature{ Status: plugins.SignatureStatusInvalid, - }, nil + }, nil, nil } if !manifest.IsV2() { return plugins.Signature{ Status: plugins.SignatureStatusInvalid, - }, nil + }, nil, nil } fsFiles, err := plugin.FS.Files() if err != nil { - return plugins.Signature{}, fmt.Errorf("files: %w", err) + return plugins.Signature{}, nil, fmt.Errorf("files: %w", err) } if len(fsFiles) == 0 { s.log.Warn("No plugin file information in directory", "pluginId", plugin.JSONData.ID) return plugins.Signature{ Status: plugins.SignatureStatusInvalid, - }, nil + }, nil, nil } // Make sure the versions all match @@ -181,20 +160,20 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu s.log.Debug("Plugin signature invalid because ID or Version mismatch", "pluginId", plugin.JSONData.ID, "manifestPluginId", manifest.Plugin, "pluginVersion", plugin.JSONData.Info.Version, "manifestPluginVersion", manifest.Version) return plugins.Signature{ Status: plugins.SignatureStatusModified, - }, nil + }, nil, nil } // Validate that plugin is running within defined root URLs if len(manifest.RootURLs) > 0 { if match, err := urlMatch(manifest.RootURLs, s.cfg.GrafanaAppURL, manifest.SignatureType); err != nil { s.log.Warn("Could not verify if root URLs match", "plugin", plugin.JSONData.ID, "rootUrls", manifest.RootURLs) - return plugins.Signature{}, err + return plugins.Signature{}, nil, err } else if !match { s.log.Warn("Could not find root URL that matches running application URL", "plugin", plugin.JSONData.ID, "appUrl", s.cfg.GrafanaAppURL, "rootUrls", manifest.RootURLs) return plugins.Signature{ Status: plugins.SignatureStatusInvalid, - }, nil + }, nil, nil } } @@ -207,7 +186,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu s.log.Debug("Plugin signature invalid", "pluginId", plugin.JSONData.ID, "error", err) return plugins.Signature{ Status: plugins.SignatureStatusModified, - }, nil + }, nil, nil } manifestFiles[p] = struct{}{} @@ -236,7 +215,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu s.log.Warn("The following files were not included in the signature", "plugin", plugin.JSONData.ID, "files", unsignedFiles) return plugins.Signature{ Status: plugins.SignatureStatusModified, - }, nil + }, nil, nil } s.log.Debug("Plugin signature valid", "id", plugin.JSONData.ID) @@ -244,7 +223,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu Status: plugins.SignatureStatusValid, Type: manifest.SignatureType, SigningOrg: manifest.SignedByOrgName, - }, nil + }, manifest, nil } func verifyHash(mlog log.Logger, plugin plugins.FoundPlugin, path, hash string) error { @@ -321,7 +300,7 @@ func (r invalidFieldErr) Error() string { return fmt.Sprintf("valid manifest field %s is required", r.field) } -func (s *Signature) validateManifest(ctx context.Context, m PluginManifest, block *clearsign.Block) error { +func (s *Signature) validateManifest(ctx context.Context, m plugins.PluginManifest, block *clearsign.Block) error { if len(m.Plugin) == 0 { return invalidFieldErr{field: "plugin"} } diff --git a/pkg/plugins/manager/signature/manifest_test.go b/pkg/plugins/manager/signature/manifest_test.go index d399268b464..5768f0f32f5 100644 --- a/pkg/plugins/manager/signature/manifest_test.go +++ b/pkg/plugins/manager/signature/manifest_test.go @@ -164,7 +164,7 @@ func TestCalculate(t *testing.T) { for _, tc := range tcs { basePath := filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin") s := provideTestServiceWithConfig(&config.PluginManagementCfg{GrafanaAppURL: tc.appURL}) - sig, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ + sig, _, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.ClassExternal }, @@ -192,7 +192,7 @@ func TestCalculate(t *testing.T) { runningWindows = true s := provideDefaultTestService() - sig, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ + sig, _, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.ClassExternal }, @@ -260,7 +260,7 @@ func TestCalculate(t *testing.T) { require.NoError(t, err) pfs, err = newPathSeparatorOverrideFS(string(tc.platform.separator), pfs) require.NoError(t, err) - sig, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ + sig, _, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.ClassExternal }, @@ -396,7 +396,7 @@ func TestFSPathSeparatorFiles(t *testing.T) { } } -func fileList(manifest *PluginManifest) []string { +func fileList(manifest *plugins.PluginManifest) []string { keys := make([]string, 0, len(manifest.Files)) for k := range manifest.Files { keys = append(keys, k) @@ -682,52 +682,52 @@ func Test_urlMatch_private(t *testing.T) { func Test_validateManifest(t *testing.T) { tcs := []struct { name string - manifest *PluginManifest + manifest *plugins.PluginManifest expectedErr string }{ { name: "Empty plugin field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.Plugin = "" }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.Plugin = "" }), expectedErr: "valid manifest field plugin is required", }, { name: "Empty keyId field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.KeyID = "" }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.KeyID = "" }), expectedErr: "valid manifest field keyId is required", }, { name: "Empty signedByOrg field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignedByOrg = "" }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.SignedByOrg = "" }), expectedErr: "valid manifest field signedByOrg is required", }, { name: "Empty signedByOrgName field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignedByOrgName = "" }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.SignedByOrgName = "" }), expectedErr: "valid manifest field SignedByOrgName is required", }, { name: "Empty signatureType field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignatureType = "" }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.SignatureType = "" }), expectedErr: "valid manifest field signatureType is required", }, { name: "Invalid signatureType field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignatureType = "invalidSignatureType" }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.SignatureType = "invalidSignatureType" }), expectedErr: "valid manifest field signatureType is required", }, { name: "Empty files field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.Files = map[string]string{} }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.Files = map[string]string{} }), expectedErr: "valid manifest field files is required", }, { name: "Empty time field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.Time = 0 }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.Time = 0 }), expectedErr: "valid manifest field time is required", }, { name: "Empty version field", - manifest: createV2Manifest(t, func(m *PluginManifest) { m.Version = "" }), + manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.Version = "" }), expectedErr: "valid manifest field version is required", }, } @@ -740,10 +740,10 @@ func Test_validateManifest(t *testing.T) { } } -func createV2Manifest(t *testing.T, cbs ...func(*PluginManifest)) *PluginManifest { +func createV2Manifest(t *testing.T, cbs ...func(*plugins.PluginManifest)) *plugins.PluginManifest { t.Helper() - m := &PluginManifest{ + m := &plugins.PluginManifest{ Plugin: "grafana-test-app", Version: "2.5.3", KeyID: "7e4d0c6a708866e7", diff --git a/pkg/plugins/pluginassets/modulehash.go b/pkg/plugins/pluginassets/modulehash.go new file mode 100644 index 00000000000..fd1ff13c578 --- /dev/null +++ b/pkg/plugins/pluginassets/modulehash.go @@ -0,0 +1,90 @@ +package pluginassets + +import ( + "encoding/base64" + "encoding/hex" + "path" + "path/filepath" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" +) + +// CalculateModuleHash calculates the module.js SHA256 hash for a plugin in the format expected by the browser for SRI checks. +// The module hash is read from the plugin's cached manifest. +// For nested plugins, the module hash is read from the root parent plugin's manifest. +// If the plugin is unsigned or not a CDN plugin, an empty string is returned. +func CalculateModuleHash(p *plugins.Plugin, cfg *config.PluginManagementCfg, cdn *pluginscdn.Service) string { + if cfg == nil || !cfg.Features.SriChecksEnabled { + return "" + } + + if !p.Signature.IsValid() { + return "" + } + + rootParent := findRootParent(p) + if rootParent.Manifest == nil { + return "" + } + + if !rootParent.Manifest.IsV2() { + return "" + } + + if !cdnEnabled(rootParent, cdn) { + return "" + } + + modulePath := getModulePathInManifest(p, rootParent) + moduleHash, ok := rootParent.Manifest.Files[modulePath] + if !ok { + return "" + } + + return convertHashForSRI(moduleHash) +} + +// findRootParent returns the root parent plugin (the one that contains the manifest). +// For non-nested plugins, it returns the plugin itself. +func findRootParent(p *plugins.Plugin) *plugins.Plugin { + root := p + for root.Parent != nil { + root = root.Parent + } + return root +} + +// getModulePathInManifest returns the path to module.js as it appears in the manifest. +// For nested plugins, this is the relative path from the root parent to the plugin's module.js. +// For non-nested plugins, this is simply "module.js". +func getModulePathInManifest(p *plugins.Plugin, rootParent *plugins.Plugin) string { + if p == rootParent { + return "module.js" + } + + // Calculate the relative path from root parent to this plugin + relPath, err := rootParent.FS.Rel(p.FS.Base()) + if err != nil { + return "" + } + + // MANIFEST.txt uses forward slashes as path separators + pluginRootPath := filepath.ToSlash(relPath) + return path.Join(pluginRootPath, "module.js") +} + +// convertHashForSRI takes a SHA256 hash string and returns it as expected by the browser for SRI checks. +func convertHashForSRI(h string) string { + hb, err := hex.DecodeString(h) + if err != nil { + return "" + } + return "sha256-" + base64.StdEncoding.EncodeToString(hb) +} + +// cdnEnabled checks if a plugin is loaded via CDN +func cdnEnabled(p *plugins.Plugin, cdn *pluginscdn.Service) bool { + return p.FS.Type().CDN() || cdn.PluginSupported(p.ID) +} diff --git a/pkg/plugins/pluginassets/modulehash_test.go b/pkg/plugins/pluginassets/modulehash_test.go new file mode 100644 index 00000000000..c11fcb7f6a9 --- /dev/null +++ b/pkg/plugins/pluginassets/modulehash_test.go @@ -0,0 +1,356 @@ +package pluginassets + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" +) + +func TestConvertHashForSRI(t *testing.T) { + for _, tc := range []struct { + hash string + expHash string + expErr bool + }{ + { + hash: "ddfcb449445064e6c39f0c20b15be3cb6a55837cf4781df23d02de005f436811", + expHash: "sha256-3fy0SURQZObDnwwgsVvjy2pVg3z0eB3yPQLeAF9DaBE=", + }, + { + hash: "not-a-valid-hash", + expErr: true, + }, + } { + t.Run(tc.hash, func(t *testing.T) { + r := convertHashForSRI(tc.hash) + if tc.expErr { + // convertHashForSRI returns empty string on error + require.Empty(t, r) + } else { + require.Equal(t, tc.expHash, r) + } + }) + } +} + +func TestCalculateModuleHash(t *testing.T) { + const ( + pluginID = "grafana-test-datasource" + parentPluginID = "grafana-test-app" + ) + + // Helper to create a plugin with manifest + createPluginWithManifest := func(id string, manifest *plugins.PluginManifest, parent *plugins.Plugin) *plugins.Plugin { + p := &plugins.Plugin{ + JSONData: plugins.JSONData{ + ID: id, + }, + Signature: plugins.SignatureStatusValid, + Manifest: manifest, + } + if parent != nil { + p.Parent = parent + } + return p + } + + // Helper to create a v2 manifest + createV2Manifest := func(files map[string]string) *plugins.PluginManifest { + return &plugins.PluginManifest{ + ManifestVersion: "2.0.0", + Files: files, + } + } + + for _, tc := range []struct { + name string + plugin *plugins.Plugin + cfg *config.PluginManagementCfg + cdn *pluginscdn.Service + expModuleHash string + }{ + { + name: "should return empty string when cfg is nil", + plugin: createPluginWithManifest(pluginID, createV2Manifest(map[string]string{ + "module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03", + }), nil), + cfg: nil, + cdn: nil, + expModuleHash: "", + }, + { + name: "should return empty string when SRI checks are disabled", + plugin: createPluginWithManifest(pluginID, createV2Manifest(map[string]string{ + "module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03", + }), nil), + cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: false}}, + cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), + expModuleHash: "", + }, + { + name: "should return empty string for unsigned plugin", + plugin: &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusUnsigned, + Manifest: createV2Manifest(map[string]string{"module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"}), + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")), + }, + cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, + cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), + expModuleHash: "", + }, + { + name: "should return module hash for valid plugin", + plugin: &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: createV2Manifest(map[string]string{"module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"}), + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")), + }, + cfg: &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.example.com", + Features: config.Features{SriChecksEnabled: true}, + PluginSettings: config.PluginSettings{ + pluginID: {"cdn": "true"}, + }, + }, + cdn: func() *pluginscdn.Service { + cfg := &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.example.com", + PluginSettings: config.PluginSettings{ + pluginID: {"cdn": "true"}, + }, + } + return pluginscdn.ProvideService(cfg) + }(), + expModuleHash: "sha256-WJG1tSLV3whtD/CxEPvZ0hu0/HFjrzTQgoai6Eb2vgM=", + }, + { + name: "should return empty string when manifest is nil", + plugin: &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: nil, + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")), + }, + cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, + cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), + expModuleHash: "", + }, + { + name: "should return empty string for v1 manifest", + plugin: &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: &plugins.PluginManifest{ + ManifestVersion: "1.0.0", + Files: map[string]string{"module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"}, + }, + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")), + }, + cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, + cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), + expModuleHash: "", + }, + { + name: "should return empty string when module.js is not in manifest", + plugin: &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: createV2Manifest(map[string]string{"plugin.json": "129fab4e0584d18c778ebdfa5fe1a68edf2e5c5aeb8290b2c68182c857cb59f8"}), + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")), + }, + cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, + cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), + expModuleHash: "", + }, + { + name: "missing module.js entry from MANIFEST.txt should not return module hash", + plugin: &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: createV2Manifest(map[string]string{"plugin.json": "129fab4e0584d18c778ebdfa5fe1a68edf2e5c5aeb8290b2c68182c857cb59f8"}), + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-module-js")), + }, + cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, + cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), + expModuleHash: "", + }, + { + name: "signed status but missing MANIFEST.txt should not return module hash", + plugin: &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: nil, + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-manifest-txt")), + }, + cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, + cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), + expModuleHash: "", + }, + { + // parentPluginID (/) + // └── pluginID (/datasource) + name: "nested plugin should return module hash from parent MANIFEST.txt", + plugin: func() *plugins.Plugin { + parent := &plugins.Plugin{ + JSONData: plugins.JSONData{ID: parentPluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: createV2Manifest(map[string]string{ + "module.js": "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a", + "datasource/module.js": "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711", + }), + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested")), + } + return &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Parent: parent, + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "datasource")), + } + }(), + cfg: &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.example.com", + Features: config.Features{SriChecksEnabled: true}, + PluginSettings: config.PluginSettings{ + pluginID: {"cdn": "true"}, + parentPluginID: {"cdn": "true"}, + }, + }, + cdn: func() *pluginscdn.Service { + cfg := &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.example.com", + PluginSettings: config.PluginSettings{ + pluginID: {"cdn": "true"}, + parentPluginID: {"cdn": "true"}, + }, + } + return pluginscdn.ProvideService(cfg) + }(), + expModuleHash: "sha256-BNcNsJHZbEd1+zK6Wo+EzCKJPrQ6/bZJcmZh1EJcZxE=", + }, + { + // parentPluginID (/) + // └── pluginID (/panels/one) + name: "nested plugin deeper than one subfolder should return module hash from parent MANIFEST.txt", + plugin: func() *plugins.Plugin { + parent := &plugins.Plugin{ + JSONData: plugins.JSONData{ID: parentPluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: createV2Manifest(map[string]string{ + "module.js": "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a", + "panels/one/module.js": "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f", + "datasource/module.js": "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711", + }), + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested")), + } + return &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Parent: parent, + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one")), + } + }(), + cfg: &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.example.com", + Features: config.Features{SriChecksEnabled: true}, + PluginSettings: config.PluginSettings{ + pluginID: {"cdn": "true"}, + parentPluginID: {"cdn": "true"}, + }, + }, + cdn: func() *pluginscdn.Service { + cfg := &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.example.com", + PluginSettings: config.PluginSettings{ + pluginID: {"cdn": "true"}, + parentPluginID: {"cdn": "true"}, + }, + } + return pluginscdn.ProvideService(cfg) + }(), + expModuleHash: "sha256-y9GsIoRkWg4emocipyn1vN0rgxIicocJxjYL7s3WFD8=", + }, + { + // grand-parent-app (/) + // ├── parent-datasource (/datasource) + // │ └── child-panel (/datasource/panels/one) + name: "nested plugin of a nested plugin should return module hash from grandparent MANIFEST.txt", + plugin: func() *plugins.Plugin { + grandparent := &plugins.Plugin{ + JSONData: plugins.JSONData{ID: "grand-parent-app"}, + Signature: plugins.SignatureStatusValid, + Manifest: createV2Manifest(map[string]string{ + "module.js": "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a", + "datasource/module.js": "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711", + "datasource/panels/one/module.js": "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f", + }), + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested")), + } + parent := &plugins.Plugin{ + JSONData: plugins.JSONData{ID: "parent-datasource"}, + Signature: plugins.SignatureStatusValid, + Parent: grandparent, + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource")), + } + return &plugins.Plugin{ + JSONData: plugins.JSONData{ID: "child-panel"}, + Signature: plugins.SignatureStatusValid, + Parent: parent, + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource", "panels", "one")), + } + }(), + cfg: &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.example.com", + Features: config.Features{SriChecksEnabled: true}, + PluginSettings: config.PluginSettings{ + "child-panel": {"cdn": "true"}, + "parent-datasource": {"cdn": "true"}, + "grand-parent-app": {"cdn": "true"}, + }, + }, + cdn: func() *pluginscdn.Service { + cfg := &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.example.com", + PluginSettings: config.PluginSettings{ + "child-panel": {"cdn": "true"}, + "parent-datasource": {"cdn": "true"}, + "grand-parent-app": {"cdn": "true"}, + }, + } + return pluginscdn.ProvideService(cfg) + }(), + expModuleHash: "sha256-y9GsIoRkWg4emocipyn1vN0rgxIicocJxjYL7s3WFD8=", + }, + { + name: "nested plugin should not return module hash when parent manifest is nil", + plugin: func() *plugins.Plugin { + parent := &plugins.Plugin{ + JSONData: plugins.JSONData{ID: parentPluginID}, + Signature: plugins.SignatureStatusValid, + Manifest: nil, // Parent has no manifest + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested")), + } + return &plugins.Plugin{ + JSONData: plugins.JSONData{ID: pluginID}, + Signature: plugins.SignatureStatusValid, + Parent: parent, + FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one")), + } + }(), + cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, + cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), + expModuleHash: "", + }, + } { + t.Run(tc.name, func(t *testing.T) { + result := CalculateModuleHash(tc.plugin, tc.cfg, tc.cdn) + require.Equal(t, tc.expModuleHash, result) + }) + } +} diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-manifest-txt/module.js b/pkg/plugins/pluginassets/testdata/module-hash-no-manifest-txt/module.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-manifest-txt/module.js rename to pkg/plugins/pluginassets/testdata/module-hash-no-manifest-txt/module.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-manifest-txt/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-no-manifest-txt/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-manifest-txt/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-no-manifest-txt/plugin.json diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/MANIFEST.txt b/pkg/plugins/pluginassets/testdata/module-hash-no-module-js/MANIFEST.txt similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/MANIFEST.txt rename to pkg/plugins/pluginassets/testdata/module-hash-no-module-js/MANIFEST.txt diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-no-module-js/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-no-module-js/plugin.json diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/something.js b/pkg/plugins/pluginassets/testdata/module-hash-no-module-js/something.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/something.js rename to pkg/plugins/pluginassets/testdata/module-hash-no-module-js/something.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/MANIFEST.txt b/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/MANIFEST.txt similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/MANIFEST.txt rename to pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/MANIFEST.txt diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/module.js b/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/module.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/module.js rename to pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/module.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/module.js b/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/module.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/module.js rename to pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/module.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/plugin.json diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/plugin.json diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/module.js b/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/module.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/module.js rename to pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/module.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/plugin.json diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/MANIFEST.txt b/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/MANIFEST.txt similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/MANIFEST.txt rename to pkg/plugins/pluginassets/testdata/module-hash-valid-nested/MANIFEST.txt diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/datasource/module.js b/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/datasource/module.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/datasource/module.js rename to pkg/plugins/pluginassets/testdata/module-hash-valid-nested/datasource/module.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/datasource/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/datasource/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/datasource/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-valid-nested/datasource/plugin.json diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/module.js b/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/module.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/module.js rename to pkg/plugins/pluginassets/testdata/module-hash-valid-nested/module.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/panels/one/module.js b/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/panels/one/module.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/panels/one/module.js rename to pkg/plugins/pluginassets/testdata/module-hash-valid-nested/panels/one/module.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/panels/one/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/panels/one/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/panels/one/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-valid-nested/panels/one/plugin.json diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-valid-nested/plugin.json diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/MANIFEST.txt b/pkg/plugins/pluginassets/testdata/module-hash-valid/MANIFEST.txt similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/MANIFEST.txt rename to pkg/plugins/pluginassets/testdata/module-hash-valid/MANIFEST.txt diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/module.js b/pkg/plugins/pluginassets/testdata/module-hash-valid/module.js similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/module.js rename to pkg/plugins/pluginassets/testdata/module-hash-valid/module.js diff --git a/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/plugin.json b/pkg/plugins/pluginassets/testdata/module-hash-valid/plugin.json similarity index 100% rename from pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/plugin.json rename to pkg/plugins/pluginassets/testdata/module-hash-valid/plugin.json diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index bf1b23a35b0..e1c26b4f87d 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -40,6 +40,7 @@ type Plugin struct { Pinned bool // Signature fields + Manifest *PluginManifest Signature SignatureStatus SignatureType SignatureType SignatureOrg string @@ -48,8 +49,9 @@ type Plugin struct { Error *Error // SystemJS fields - Module string - BaseURL string + Module string + ModuleHash string + BaseURL string Angular AngularMeta @@ -532,3 +534,24 @@ func (pt Type) IsValid() bool { } return false } + +// PluginManifest holds details for the file manifest +type PluginManifest struct { + Plugin string `json:"plugin"` + Version string `json:"version"` + KeyID string `json:"keyId"` + Time int64 `json:"time"` + Files map[string]string `json:"files"` + + // V2 supported fields + ManifestVersion string `json:"manifestVersion"` + SignatureType SignatureType `json:"signatureType"` + SignedByOrg string `json:"signedByOrg"` + SignedByOrgName string `json:"signedByOrgName"` + RootURLs []string `json:"rootUrls"` +} + +// IsV2 returns true if the manifest is version 2.x +func (m *PluginManifest) IsV2() bool { + return strings.HasPrefix(m.ManifestVersion, "2.") +} diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 218e9fabc36..5b4f3bed5bb 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -376,7 +376,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api keyretrieverService := keyretriever.ProvideService(keyRetriever) signatureSignature := signature.ProvideService(pluginManagementCfg, keyretrieverService) localProvider := pluginassets.NewLocalProvider() - bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider) + pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg) + bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider, pluginscdnService) unsignedPluginAuthorizer := signature.ProvideOSSAuthorizer(pluginManagementCfg) validation := signature.ProvideValidatorService(unsignedPluginAuthorizer) angularpatternsstoreService := angularpatternsstore.ProvideService(kvStore) @@ -714,8 +715,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg) - pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, signatureSignature, pluginstoreService) + pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, pluginstoreService) avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg) prefService := prefimpl.ProvideService(sqlStore, cfg) dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider) @@ -1042,7 +1042,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac keyretrieverService := keyretriever.ProvideService(keyRetriever) signatureSignature := signature.ProvideService(pluginManagementCfg, keyretrieverService) localProvider := pluginassets.NewLocalProvider() - bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider) + pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg) + bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider, pluginscdnService) unsignedPluginAuthorizer := signature.ProvideOSSAuthorizer(pluginManagementCfg) validation := signature.ProvideValidatorService(unsignedPluginAuthorizer) angularpatternsstoreService := angularpatternsstore.ProvideService(kvStore) @@ -1382,8 +1383,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg) - pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, signatureSignature, pluginstoreService) + pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, pluginstoreService) avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg) prefService := prefimpl.ProvideService(sqlStore, cfg) dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider) diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index a5a7a7fd8db..eee04acedfc 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -26,6 +26,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/pluginassets" "github.com/grafana/grafana/pkg/plugins/pluginerrs" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins" @@ -213,10 +214,27 @@ func TestLoader_Load(t *testing.T) { ExtensionPoints: []plugins.ExtensionPoint{}, }, }, - Class: plugins.ClassExternal, - Module: "public/plugins/test-app/module.js", - BaseURL: "public/plugins/test-app", - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "includes-symlinks")), + Class: plugins.ClassExternal, + Module: "public/plugins/test-app/module.js", + BaseURL: "public/plugins/test-app", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "includes-symlinks")), + Manifest: &plugins.PluginManifest{ + Plugin: "test-app", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1622547655175, + Files: map[string]string{ + "dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303", + "dashboards/extra/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee", + "plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf", + "symlink_to_txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048", + "text.txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypeGrafana, + SignedByOrg: "grafana", + SignedByOrgName: "Grafana Labs", + }, Signature: "valid", SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -647,10 +665,24 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { Executable: "test", State: plugins.ReleaseStateAlpha, }, - Class: plugins.ClassExternal, - Module: "public/plugins/test-datasource/module.js", - BaseURL: "public/plugins/test-datasource", - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature/plugin")), + Class: plugins.ClassExternal, + Module: "public/plugins/test-datasource/module.js", + BaseURL: "public/plugins/test-datasource", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature/plugin")), + Manifest: &plugins.PluginManifest{ + Plugin: "test-datasource", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1661171417046, + Files: map[string]string{ + "plugin.json": "203ef4a613c5693c437a665cd67f95e2756a0f71b336b2ffb265db7c180d0b19", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypePrivate, + SignedByOrg: "willbrowne", + SignedByOrgName: "Will Browne", + RootURLs: []string{"http://localhost:3000/"}, + }, Signature: "valid", SignatureType: plugins.SignatureTypePrivate, SignatureOrg: "Will Browne", @@ -767,8 +799,22 @@ func TestLoader_Load_RBACReady(t *testing.T) { }, Backend: false, }, - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app-with-roles")), - Class: plugins.ClassExternal, + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app-with-roles")), + Class: plugins.ClassExternal, + Manifest: &plugins.PluginManifest{ + Plugin: "test-app", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1667484928676, + Files: map[string]string{ + "plugin.json": "3348335ec100392b325f3eeb882a07c729e9cbf0f1ae331239f46840bb1a01eb", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypePrivate, + SignedByOrg: "gabrielmabille", + SignedByOrgName: "gabrielmabille", + RootURLs: []string{"http://localhost:3000/"}, + }, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypePrivate, SignatureOrg: "gabrielmabille", @@ -837,8 +883,22 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) { Backend: true, Executable: "test", }, - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature-root-url-uri/plugin")), - Class: plugins.ClassExternal, + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature-root-url-uri/plugin")), + Class: plugins.ClassExternal, + Manifest: &plugins.PluginManifest{ + Plugin: "test-datasource", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1661171981629, + Files: map[string]string{ + "plugin.json": "203ef4a613c5693c437a665cd67f95e2756a0f71b336b2ffb265db7c180d0b19", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypePrivate, + SignedByOrg: "willbrowne", + SignedByOrgName: "Will Browne", + RootURLs: []string{"http://localhost:3000/grafana"}, + }, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypePrivate, SignatureOrg: "Will Browne", @@ -925,8 +985,24 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) { }, Backend: false, }, - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app")), - Class: plugins.ClassExternal, + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app")), + Class: plugins.ClassExternal, + Manifest: &plugins.PluginManifest{ + Plugin: "test-app", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1621356785895, + Files: map[string]string{ + "plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf", + "dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303", + "dashboards/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee", + "dashboards/connections_result.json": "124d85c9c2e40214b83273f764574937a79909cfac3f925276fbb72543c224dc", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypeGrafana, + SignedByOrg: "grafana", + SignedByOrgName: "Grafana Labs", + }, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1017,8 +1093,24 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) { }, Backend: false, }, - FS: mustNewStaticFSForTests(t, pluginDir1), - Class: plugins.ClassExternal, + FS: mustNewStaticFSForTests(t, pluginDir1), + Class: plugins.ClassExternal, + Manifest: &plugins.PluginManifest{ + Plugin: "test-app", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1621356785895, + Files: map[string]string{ + "plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf", + "dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303", + "dashboards/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee", + "dashboards/connections_result.json": "124d85c9c2e40214b83273f764574937a79909cfac3f925276fbb72543c224dc", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypeGrafana, + SignedByOrg: "grafana", + SignedByOrgName: "Grafana Labs", + }, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1180,9 +1272,23 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }, Backend: true, }, - Module: "public/plugins/test-datasource/module.js", - BaseURL: "public/plugins/test-datasource", - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent")), + Module: "public/plugins/test-datasource/module.js", + BaseURL: "public/plugins/test-datasource", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent")), + Manifest: &plugins.PluginManifest{ + Plugin: "test-datasource", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1661172777367, + Files: map[string]string{ + "plugin.json": "a029469ace740e9502bfb0d40924d1cccae73d0b18adcd8f1ceb7f17bf36beb8", + "nested/plugin.json": "e64abd35cd211e0e4682974ad5cdd1be7a0b7cd24951d302a16d9e2cb6cefea4", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypeGrafana, + SignedByOrg: "grafana", + SignedByOrgName: "Grafana Labs", + }, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1225,9 +1331,23 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { ExtensionPoints: []plugins.ExtensionPoint{}, }, }, - Module: "public/plugins/test-panel/module.js", - BaseURL: "public/plugins/test-panel", - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent/nested")), + Module: "public/plugins/test-panel/module.js", + BaseURL: "public/plugins/test-panel", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent/nested")), + Manifest: &plugins.PluginManifest{ + Plugin: "test-datasource", + Version: "1.0.0", + KeyID: "7e4d0c6a708866e7", + Time: 1661172777367, + Files: map[string]string{ + "plugin.json": "a029469ace740e9502bfb0d40924d1cccae73d0b18adcd8f1ceb7f17bf36beb8", + "nested/plugin.json": "e64abd35cd211e0e4682974ad5cdd1be7a0b7cd24951d302a16d9e2cb6cefea4", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypeGrafana, + SignedByOrg: "grafana", + SignedByOrgName: "Grafana Labs", + }, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1375,10 +1495,25 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }, Backend: false, }, - Module: "public/plugins/myorgid-simple-app/module.js", - BaseURL: "public/plugins/myorgid-simple-app", - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist")), - DefaultNavURL: "/plugins/myorgid-simple-app/page/root-page-react", + Module: "public/plugins/myorgid-simple-app/module.js", + BaseURL: "public/plugins/myorgid-simple-app", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist")), + DefaultNavURL: "/plugins/myorgid-simple-app/page/root-page-react", + Manifest: &plugins.PluginManifest{ + Plugin: "myorgid-simple-app", + Version: "%VERSION%", + KeyID: "7e4d0c6a708866e7", + Time: 1642614241713, + Files: map[string]string{ + "plugin.json": "1abecfd0229814f6c284ff3c8dd744548f8d676ab3250cd7902c99dabf11480e", + "child/plugin.json": "66ba0dffaf3b1bfa17eb9a8672918fc66d1001f465b1061f4fc19c2f2c100f51", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypeGrafana, + SignedByOrg: "grafana", + SignedByOrgName: "Grafana Labs", + RootURLs: []string{}, + }, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1431,6 +1566,21 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { BaseURL: "public/plugins/myorgid-simple-panel", FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist/child")), IncludedInAppID: parent.ID, + Manifest: &plugins.PluginManifest{ + Plugin: "myorgid-simple-app", + Version: "%VERSION%", + KeyID: "7e4d0c6a708866e7", + Time: 1642614241713, + Files: map[string]string{ + "plugin.json": "1abecfd0229814f6c284ff3c8dd744548f8d676ab3250cd7902c99dabf11480e", + "child/plugin.json": "66ba0dffaf3b1bfa17eb9a8672918fc66d1001f465b1061f4fc19c2f2c100f51", + }, + ManifestVersion: "2.0.0", + SignatureType: plugins.SignatureTypeGrafana, + SignedByOrg: "grafana", + SignedByOrgName: "Grafana Labs", + RootURLs: []string{}, + }, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1484,7 +1634,7 @@ func newLoader(t *testing.T, cfg *config.PluginManagementCfg, reg registry.Servi require.NoError(t, err) return ProvideService(cfg, pipeline.ProvideDiscoveryStage(cfg, reg), - pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginAssetsProvider), + pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginAssetsProvider, pluginscdn.ProvideService(cfg)), pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector), pipeline.ProvideInitializationStage(cfg, reg, backendFactory, proc, &pluginfakes.FakeAuthService{}, pluginfakes.NewFakeRoleRegistry(), pluginfakes.NewFakeActionSetRegistry(), pluginfakes.NewFakePluginEnvProvider(), tracing.InitializeTracerForTest(), provisionedplugins.NewNoop()), terminate, errTracker) @@ -1514,7 +1664,7 @@ func newLoaderWithOpts(t *testing.T, cfg *config.PluginManagementCfg, opts loade } return ProvideService(cfg, pipeline.ProvideDiscoveryStage(cfg, reg), - pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginassets.NewLocalProvider()), + pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginassets.NewLocalProvider(), pluginscdn.ProvideService(cfg)), pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector), pipeline.ProvideInitializationStage(cfg, reg, backendFactoryProvider, proc, authServiceRegistry, pluginfakes.NewFakeRoleRegistry(), pluginfakes.NewFakeActionSetRegistry(), pluginfakes.NewFakePluginEnvProvider(), tracing.InitializeTracerForTest(), provisionedplugins.NewNoop()), terminate, errTracker) diff --git a/pkg/services/pluginsintegration/pipeline/pipeline.go b/pkg/services/pluginsintegration/pipeline/pipeline.go index f377e9eb867..94b5331ac00 100644 --- a/pkg/services/pluginsintegration/pipeline/pipeline.go +++ b/pkg/services/pluginsintegration/pipeline/pipeline.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/pluginassets" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/pluginsintegration/coreplugin" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" "github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins" @@ -42,7 +43,7 @@ func ProvideDiscoveryStage(cfg *config.PluginManagementCfg, pr registry.Service) }) } -func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.SignatureCalculator, ap pluginassets.Provider) *bootstrap.Bootstrap { +func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.SignatureCalculator, ap pluginassets.Provider, cdn *pluginscdn.Service) *bootstrap.Bootstrap { disableAlertingForTempoDecorateFunc := func(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { if p.ID == coreplugin.Tempo && !cfg.Features.TempoAlertingEnabled { p.Alerting = false @@ -52,7 +53,7 @@ func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.Signature return bootstrap.New(cfg, bootstrap.Opts{ ConstructFunc: bootstrap.DefaultConstructFunc(cfg, sc, ap), - DecorateFuncs: append(bootstrap.DefaultDecorateFuncs(cfg), disableAlertingForTempoDecorateFunc), + DecorateFuncs: append(bootstrap.DefaultDecorateFuncs(cfg, cdn), disableAlertingForTempoDecorateFunc), }) } diff --git a/pkg/services/pluginsintegration/pluginassets/pluginassets.go b/pkg/services/pluginsintegration/pluginassets/pluginassets.go index 4d9a7ec1a53..8735f7a4354 100644 --- a/pkg/services/pluginsintegration/pluginassets/pluginassets.go +++ b/pkg/services/pluginsintegration/pluginassets/pluginassets.go @@ -2,19 +2,12 @@ package pluginassets import ( "context" - "encoding/base64" - "encoding/hex" - "fmt" - "path" - "path/filepath" - "sync" "github.com/Masterminds/semver/v3" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" - "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" ) @@ -28,24 +21,20 @@ var ( scriptLoadingMinSupportedVersion = semver.MustParse(CreatePluginVersionScriptSupportEnabled) ) -func ProvideService(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service, sig *signature.Signature, store pluginstore.Store) *Service { +func ProvideService(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service, store pluginstore.Store) *Service { return &Service{ - cfg: cfg, - cdn: cdn, - signature: sig, - store: store, - log: log.New("pluginassets"), + cfg: cfg, + cdn: cdn, + store: store, + log: log.New("pluginassets"), } } type Service struct { - cfg *config.PluginManagementCfg - cdn *pluginscdn.Service - signature *signature.Signature - store pluginstore.Store - log log.Logger - - moduleHashCache sync.Map + cfg *config.PluginManagementCfg + cdn *pluginscdn.Service + store pluginstore.Store + log log.Logger } // LoadingStrategy calculates the loading strategy for a plugin. @@ -82,95 +71,6 @@ func (s *Service) LoadingStrategy(_ context.Context, p pluginstore.Plugin) plugi return plugins.LoadingStrategyFetch } -// ModuleHash returns the module.js SHA256 hash for a plugin in the format expected by the browser for SRI checks. -// The module hash is read from the plugin's MANIFEST.txt file. -// The plugin can also be a nested plugin. -// If the plugin is unsigned, an empty string is returned. -// The results are cached to avoid repeated reads from the MANIFEST.txt file. -func (s *Service) ModuleHash(ctx context.Context, p pluginstore.Plugin) string { - k := s.moduleHashCacheKey(p) - cachedValue, ok := s.moduleHashCache.Load(k) - if ok { - return cachedValue.(string) - } - mh, err := s.moduleHash(ctx, p, "") - if err != nil { - s.log.Error("Failed to calculate module hash", "plugin", p.ID, "error", err) - } - s.moduleHashCache.Store(k, mh) - return mh -} - -// moduleHash is the underlying function for ModuleHash. See its documentation for more information. -// If the plugin is not a CDN plugin, the function will return an empty string. -// It will read the module hash from the MANIFEST.txt in the [[plugins.FS]] of the provided plugin. -// If childFSBase is provided, the function will try to get the hash from MANIFEST.txt for the provided children's -// module.js file, rather than for the provided plugin. -func (s *Service) moduleHash(ctx context.Context, p pluginstore.Plugin, childFSBase string) (r string, err error) { - if !s.cfg.Features.SriChecksEnabled { - return "", nil - } - - // Ignore unsigned plugins - if !p.Signature.IsValid() { - return "", nil - } - - if p.Parent != nil { - // Nested plugin - parent, ok := s.store.Plugin(ctx, p.Parent.ID) - if !ok { - return "", fmt.Errorf("parent plugin plugin %q for child plugin %q not found", p.Parent.ID, p.ID) - } - - // The module hash is contained within the parent's MANIFEST.txt file. - // For example, the parent's MANIFEST.txt will contain an entry similar to this: - // - // ``` - // "datasource/module.js": "1234567890abcdef..." - // ``` - // - // Recursively call moduleHash with the parent plugin and with the children plugin folder path - // to get the correct module hash for the nested plugin. - if childFSBase == "" { - childFSBase = p.Base() - } - return s.moduleHash(ctx, parent, childFSBase) - } - - // Only CDN plugins are supported for SRI checks. - // CDN plugins have the version as part of the URL, which acts as a cache-buster. - // Needed due to: https://github.com/grafana/plugin-tools/pull/1426 - // FS plugins build before this change will have SRI mismatch issues. - if !s.cdnEnabled(p.ID, p.FS) { - return "", nil - } - - manifest, err := s.signature.ReadPluginManifestFromFS(ctx, p.FS) - if err != nil { - return "", fmt.Errorf("read plugin manifest: %w", err) - } - if !manifest.IsV2() { - return "", nil - } - - var childPath string - if childFSBase != "" { - // Calculate the relative path of the child plugin folder from the parent plugin folder. - childPath, err = p.FS.Rel(childFSBase) - if err != nil { - return "", fmt.Errorf("rel path: %w", err) - } - // MANIFETS.txt uses forward slashes as path separators. - childPath = filepath.ToSlash(childPath) - } - moduleHash, ok := manifest.Files[path.Join(childPath, "module.js")] - if !ok { - return "", nil - } - return convertHashForSRI(moduleHash) -} - func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool { if cpv, ok := ps[CreatePluginVersionCfgKey]; ok { createPluginVer, err := semver.NewVersion(cpv) @@ -188,17 +88,3 @@ func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool { func (s *Service) cdnEnabled(pluginID string, fs plugins.FS) bool { return s.cdn.PluginSupported(pluginID) || fs.Type().CDN() } - -// convertHashForSRI takes a SHA256 hash string and returns it as expected by the browser for SRI checks. -func convertHashForSRI(h string) (string, error) { - hb, err := hex.DecodeString(h) - if err != nil { - return "", fmt.Errorf("hex decode string: %w", err) - } - return "sha256-" + base64.StdEncoding.EncodeToString(hb), nil -} - -// moduleHashCacheKey returns a unique key for the module hash cache. -func (s *Service) moduleHashCacheKey(p pluginstore.Plugin) string { - return p.ID + ":" + p.Info.Version -} diff --git a/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go b/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go index 192717c34ff..91b8b515bb1 100644 --- a/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go +++ b/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go @@ -2,19 +2,14 @@ package pluginassets import ( "context" - "fmt" - "path/filepath" "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" - "github.com/grafana/grafana/pkg/plugins/manager/signature" - "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" ) @@ -179,349 +174,6 @@ func TestService_Calculate(t *testing.T) { } } -func TestService_ModuleHash(t *testing.T) { - const ( - pluginID = "grafana-test-datasource" - parentPluginID = "grafana-test-app" - ) - for _, tc := range []struct { - name string - features *config.Features - store []pluginstore.Plugin - - // Can be used to configure plugin's fs - // fs cdn type = loaded from CDN with no files on disk - // fs local type = files on disk but served from CDN only if cdn=true - plugin pluginstore.Plugin - - // When true, set cdn=true in config - cdn bool - expModuleHash string - }{ - { - name: "unsigned should not return module hash", - plugin: newPlugin(pluginID, withSignatureStatus(plugins.SignatureStatusUnsigned)), - cdn: false, - features: &config.Features{SriChecksEnabled: false}, - expModuleHash: "", - }, - { - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - withClass(plugins.ClassExternal), - ), - cdn: true, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"), - }, - { - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - withClass(plugins.ClassExternal), - ), - cdn: true, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"), - }, - { - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - ), - cdn: false, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: "", - }, - { - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - ), - cdn: true, - features: &config.Features{SriChecksEnabled: false}, - expModuleHash: "", - }, - { - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - ), - cdn: false, - features: &config.Features{SriChecksEnabled: false}, - expModuleHash: "", - }, - { - // parentPluginID (/) - // └── pluginID (/datasource) - name: "nested plugin should return module hash from parent MANIFEST.txt", - store: []pluginstore.Plugin{ - newPlugin( - parentPluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))), - ), - }, - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "datasource"))), - withParent(parentPluginID), - ), - cdn: true, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: newSRIHash(t, "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711"), - }, - { - // parentPluginID (/) - // └── pluginID (/panels/one) - name: "nested plugin deeper than one subfolder should return module hash from parent MANIFEST.txt", - store: []pluginstore.Plugin{ - newPlugin( - parentPluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))), - ), - }, - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one"))), - withParent(parentPluginID), - ), - cdn: true, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: newSRIHash(t, "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f"), - }, - { - // grand-parent-app (/) - // ├── parent-datasource (/datasource) - // │ └── child-panel (/datasource/panels/one) - name: "nested plugin of a nested plugin should return module hash from parent MANIFEST.txt", - store: []pluginstore.Plugin{ - newPlugin( - "grand-parent-app", - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested"))), - ), - newPlugin( - "parent-datasource", - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource"))), - withParent("grand-parent-app"), - ), - }, - plugin: newPlugin( - "child-panel", - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource", "panels", "one"))), - withParent("parent-datasource"), - ), - cdn: true, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: newSRIHash(t, "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f"), - }, - { - name: "nested plugin should not return module hash from parent if it's not registered in the store", - store: []pluginstore.Plugin{}, - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one"))), - withParent(parentPluginID), - ), - cdn: false, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: "", - }, - { - name: "missing module.js entry from MANIFEST.txt should not return module hash", - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-module-js"))), - ), - cdn: false, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: "", - }, - { - name: "signed status but missing MANIFEST.txt should not return module hash", - plugin: newPlugin( - pluginID, - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-manifest-txt"))), - ), - cdn: false, - features: &config.Features{SriChecksEnabled: true}, - expModuleHash: "", - }, - } { - if tc.name == "" { - var expS string - if tc.expModuleHash == "" { - expS = "should not return module hash" - } else { - expS = "should return module hash" - } - tc.name = fmt.Sprintf("feature=%v, cdn_config=%v, class=%v %s", tc.features.SriChecksEnabled, tc.cdn, tc.plugin.Class, expS) - } - - t.Run(tc.name, func(t *testing.T) { - var pluginSettings config.PluginSettings - if tc.cdn { - pluginSettings = config.PluginSettings{ - pluginID: { - "cdn": "true", - }, - parentPluginID: map[string]string{ - "cdn": "true", - }, - "grand-parent-app": map[string]string{ - "cdn": "true", - }, - } - } - features := tc.features - if features == nil { - features = &config.Features{} - } - pCfg := &config.PluginManagementCfg{ - PluginsCDNURLTemplate: "http://cdn.example.com", - PluginSettings: pluginSettings, - Features: *features, - } - svc := ProvideService( - pCfg, - pluginscdn.ProvideService(pCfg), - signature.ProvideService(pCfg, statickey.New()), - pluginstore.NewFakePluginStore(tc.store...), - ) - mh := svc.ModuleHash(context.Background(), tc.plugin) - require.Equal(t, tc.expModuleHash, mh) - }) - } -} - -func TestService_ModuleHash_Cache(t *testing.T) { - pCfg := &config.PluginManagementCfg{ - PluginSettings: config.PluginSettings{}, - Features: config.Features{SriChecksEnabled: true}, - } - svc := ProvideService( - pCfg, - pluginscdn.ProvideService(pCfg), - signature.ProvideService(pCfg, statickey.New()), - pluginstore.NewFakePluginStore(), - ) - const pluginID = "grafana-test-datasource" - - t.Run("cache key", func(t *testing.T) { - t.Run("with version", func(t *testing.T) { - const pluginVersion = "1.0.0" - p := newPlugin(pluginID, withInfo(plugins.Info{Version: pluginVersion})) - k := svc.moduleHashCacheKey(p) - require.Equal(t, pluginID+":"+pluginVersion, k, "cache key should be correct") - }) - - t.Run("without version", func(t *testing.T) { - p := newPlugin(pluginID) - k := svc.moduleHashCacheKey(p) - require.Equal(t, pluginID+":", k, "cache key should be correct") - }) - }) - - t.Run("ModuleHash usage", func(t *testing.T) { - pV1 := newPlugin( - pluginID, - withInfo(plugins.Info{Version: "1.0.0"}), - withSignatureStatus(plugins.SignatureStatusValid), - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - ) - - pCfg = &config.PluginManagementCfg{ - PluginsCDNURLTemplate: "https://cdn.grafana.com", - PluginSettings: config.PluginSettings{ - pluginID: { - "cdn": "true", - }, - }, - Features: config.Features{SriChecksEnabled: true}, - } - svc = ProvideService( - pCfg, - pluginscdn.ProvideService(pCfg), - signature.ProvideService(pCfg, statickey.New()), - pluginstore.NewFakePluginStore(), - ) - - k := svc.moduleHashCacheKey(pV1) - - _, ok := svc.moduleHashCache.Load(k) - require.False(t, ok, "cache should initially be empty") - - mhV1 := svc.ModuleHash(context.Background(), pV1) - pV1Exp := newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03") - require.Equal(t, pV1Exp, mhV1, "returned value should be correct") - - cachedMh, ok := svc.moduleHashCache.Load(k) - require.True(t, ok) - require.Equal(t, pV1Exp, cachedMh, "cache should contain the returned value") - - t.Run("different version uses different cache key", func(t *testing.T) { - pV2 := newPlugin( - pluginID, - withInfo(plugins.Info{Version: "2.0.0"}), - withSignatureStatus(plugins.SignatureStatusValid), - // different fs for different hash - withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))), - ) - mhV2 := svc.ModuleHash(context.Background(), pV2) - require.NotEqual(t, mhV2, mhV1, "different version should have different hash") - require.Equal(t, newSRIHash(t, "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a"), mhV2) - }) - - t.Run("cache should be used", func(t *testing.T) { - // edit cache directly - svc.moduleHashCache.Store(k, "hax") - require.Equal(t, "hax", svc.ModuleHash(context.Background(), pV1)) - }) - }) -} - -func TestConvertHashFromSRI(t *testing.T) { - for _, tc := range []struct { - hash string - expHash string - expErr bool - }{ - { - hash: "ddfcb449445064e6c39f0c20b15be3cb6a55837cf4781df23d02de005f436811", - expHash: "sha256-3fy0SURQZObDnwwgsVvjy2pVg3z0eB3yPQLeAF9DaBE=", - }, - { - hash: "not-a-valid-hash", - expErr: true, - }, - } { - t.Run(tc.hash, func(t *testing.T) { - r, err := convertHashForSRI(tc.hash) - if tc.expErr { - require.Error(t, err) - } else { - require.NoError(t, err) - require.Equal(t, tc.expHash, r) - } - }) - } -} - func newPlugin(pluginID string, cbs ...func(p pluginstore.Plugin) pluginstore.Plugin) pluginstore.Plugin { p := pluginstore.Plugin{ JSONData: plugins.JSONData{ @@ -534,13 +186,6 @@ func newPlugin(pluginID string, cbs ...func(p pluginstore.Plugin) pluginstore.Pl return p } -func withInfo(info plugins.Info) func(p pluginstore.Plugin) pluginstore.Plugin { - return func(p pluginstore.Plugin) pluginstore.Plugin { - p.Info = info - return p - } -} - func withFS(fs plugins.FS) func(p pluginstore.Plugin) pluginstore.Plugin { return func(p pluginstore.Plugin) pluginstore.Plugin { p.FS = fs @@ -548,13 +193,6 @@ func withFS(fs plugins.FS) func(p pluginstore.Plugin) pluginstore.Plugin { } } -func withSignatureStatus(status plugins.SignatureStatus) func(p pluginstore.Plugin) pluginstore.Plugin { - return func(p pluginstore.Plugin) pluginstore.Plugin { - p.Signature = status - return p - } -} - func withAngular(angular bool) func(p pluginstore.Plugin) pluginstore.Plugin { return func(p pluginstore.Plugin) pluginstore.Plugin { p.Angular = plugins.AngularMeta{Detected: angular} @@ -562,13 +200,6 @@ func withAngular(angular bool) func(p pluginstore.Plugin) pluginstore.Plugin { } } -func withParent(parentID string) func(p pluginstore.Plugin) pluginstore.Plugin { - return func(p pluginstore.Plugin) pluginstore.Plugin { - p.Parent = &pluginstore.ParentPlugin{ID: parentID} - return p - } -} - func withClass(class plugins.Class) func(p pluginstore.Plugin) pluginstore.Plugin { return func(p pluginstore.Plugin) pluginstore.Plugin { p.Class = class @@ -587,9 +218,3 @@ func newPluginSettings(pluginID string, kv map[string]string) config.PluginSetti pluginID: kv, } } - -func newSRIHash(t *testing.T, s string) string { - r, err := convertHashForSRI(s) - require.NoError(t, err) - return r -} diff --git a/pkg/services/pluginsintegration/pluginstore/plugins.go b/pkg/services/pluginsintegration/pluginstore/plugins.go index f4504d254c5..77fe1365fc3 100644 --- a/pkg/services/pluginsintegration/pluginstore/plugins.go +++ b/pkg/services/pluginsintegration/pluginstore/plugins.go @@ -30,8 +30,9 @@ type Plugin struct { Error *plugins.Error // SystemJS fields - Module string - BaseURL string + Module string + BaseURL string + ModuleHash string Angular plugins.AngularMeta @@ -80,6 +81,7 @@ func ToGrafanaDTO(p *plugins.Plugin) Plugin { ExternalService: p.ExternalService, Angular: p.Angular, Translations: p.Translations, + ModuleHash: p.ModuleHash, } if p.Parent != nil { diff --git a/pkg/services/pluginsintegration/test_helper.go b/pkg/services/pluginsintegration/test_helper.go index 9daad43e3e2..9957fc11b22 100644 --- a/pkg/services/pluginsintegration/test_helper.go +++ b/pkg/services/pluginsintegration/test_helper.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/pluginassets" "github.com/grafana/grafana/pkg/plugins/pluginerrs" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/coreplugin" "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" @@ -49,7 +50,7 @@ func CreateIntegrationTestCtx(t *testing.T, cfg *setting.Cfg, coreRegistry *core proc := process.ProvideService() disc := pipeline.ProvideDiscoveryStage(pCfg, reg) - boot := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), pluginassets.NewLocalProvider()) + boot := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), pluginassets.NewLocalProvider(), pluginscdn.ProvideService(pCfg)) valid := pipeline.ProvideValidationStage(pCfg, signature.NewValidator(signature.NewUnsignedAuthorizer(pCfg)), angularInspector) init := pipeline.ProvideInitializationStage(pCfg, reg, coreplugin.ProvideCoreProvider(coreRegistry), proc, &pluginfakes.FakeAuthService{}, pluginfakes.NewFakeRoleRegistry(), pluginfakes.NewFakeActionSetRegistry(), nil, tracing.InitializeTracerForTest(), provisionedplugins.NewNoop()) term, err := pipeline.ProvideTerminationStage(pCfg, reg, proc) @@ -87,7 +88,7 @@ func CreateTestLoader(t *testing.T, cfg *pluginsCfg.PluginManagementCfg, opts Lo } if opts.Bootstrapper == nil { - opts.Bootstrapper = pipeline.ProvideBootstrapStage(cfg, signature.ProvideService(cfg, statickey.New()), pluginassets.NewLocalProvider()) + opts.Bootstrapper = pipeline.ProvideBootstrapStage(cfg, signature.ProvideService(cfg, statickey.New()), pluginassets.NewLocalProvider(), pluginscdn.ProvideService(cfg)) } if opts.Validator == nil { From f5aa39cc27343026d797cfefe5c6e68c1c838a1a Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Tue, 13 Jan 2026 07:38:35 -0700 Subject: [PATCH 05/19] Dashboard Conversion: Add missing dev dashboards when comparing v1 to v2 conversion (#115967) * descritpion and transformations * add fieldMinMax to schema * add nullValueMode to schema * convert actions * disabled prop in transformations * update * fix * gauge * remove index (deprecated) and decimals as strings * gofmt * codegen * lint * open api --- .../kinds/v2alpha1/dashboard_spec.cue | 10 + .../kinds/v2beta1/dashboard_spec.cue | 10 + .../dashboard/v2alpha1/dashboard_spec.cue | 10 + .../dashboard/v2alpha1/dashboard_spec_gen.go | 15 + .../v2alpha1/zz_generated.openapi.go | 14 + .../apis/dashboard/v2beta1/dashboard_spec.cue | 10 + .../dashboard/v2beta1/dashboard_spec_gen.go | 15 + .../dashboard/v2beta1/zz_generated.openapi.go | 14 + apps/dashboard/pkg/apis/dashboard_manifest.go | 4 +- .../v1beta1.annotation-filtering.v42.json | 427 ++++++++++++++++++ ...> v1beta1.multi-lane-annotations.v42.json} | 0 ...=> v1beta1.elasticsearch_complex.v42.json} | 0 ... v1beta1.elasticsearch_migration.v42.json} | 0 ... => v1beta1.elasticsearch_simple.v42.json} | 0 ...42.json => v1beta1.influxdb-logs.v42.json} | 0 ...on => v1beta1.influxdb-templated.v42.json} | 0 ...42.json => v1beta1.loki_fakedata.v42.json} | 0 ... => v1beta1.loki_query_splitting.v42.json} | 24 +- ...2.json => v1beta1.mssql_fakedata.v42.json} | 0 ...2.json => v1beta1.mssql_unittest.v42.json} | 0 ...2.json => v1beta1.mysql_fakedata.v42.json} | 0 ...2.json => v1beta1.mysql_unittest.v42.json} | 0 ...sdb.v42.json => v1beta1.opentsdb.v42.json} | 0 ...v42.json => v1beta1.opentsdb_v23.v42.json} | 0 ...son => v1beta1.postgres_fakedata.v42.json} | 0 ...son => v1beta1.postgres_unittest.v42.json} | 0 ....json => v1beta1.bar-gauge-demo2.v42.json} | 0 ....demo1.v42.json => v1beta1.demo1.v42.json} | 0 ...n => v1beta1.new_features_in_v74.v42.json} | 0 ...on => v1beta1.new_features_in_v8.v42.json} | 0 ...> v1beta1.Repeating-Kitchen-Sink.v42.json} | 0 ...1.Repeating-a-panel-horizontally.v42.json} | 0 ...ta1.Repeating-a-panel-vertically.v42.json} | 0 ...ith-a-repeating-horizontal-panel.v42.json} | 0 ...-with-a-repeating-vertical-panel.v42.json} | 0 ...> v1beta1.Repeating-an-empty-row.v42.json} | 0 ... v1beta1.link-onclick-extensions.v42.json} | 0 ... => v1beta1.link-path-extensions.v42.json} | 0 ....json => v1beta1.datadata-macros.v42.json} | 0 ...obal-variables-and-interpolation.v42.json} | 0 ...ng-dashboard-links-and-variables.v42.json} | 0 ...eta1.templating-repeating-panels.v42.json} | 0 ...1beta1.templating-repeating-rows.v42.json} | 0 ...templating-textbox-e2e-scenarios.v42.json} | 0 ...on => v1beta1.testdata-datalinks.v42.json} | 0 ...tdata-nested-variables-drilldown.v42.json} | 0 ...1beta1.testdata-nested-variables.v42.json} | 0 ...a1.testdata-test-variable-output.v42.json} | 0 ...beta1.testdata-variables-textbox.v42.json} | 0 ...ables-that-update-on-time-change.v42.json} | 0 ...n => v1beta1.live-flakey-refresh.v42.json} | 0 ....v42.json => v1beta1.live-flakey.v42.json} | 0 ...v42.json => v1beta1.live-publish.v42.json} | 0 ...v42.json => v1beta1.live-streams.v42.json} | 0 ...s.v42.json => v1beta1.migrations.v42.json} | 0 ...n => v1beta1.barchart-autosizing.v42.json} | 0 ...barchart-label-rotation-skipping.v42.json} | 0 ...> v1beta1.barchart-series-toggle.v42.json} | 0 ...ta1.barchart-thresholds-mappings.v42.json} | 0 ...1beta1.barchart-tooltips-legends.v42.json} | 0 ...2.json => v1beta1.bar_gauge_demo.v42.json} | 0 ...=> v1beta1.panel_tests_bar_gauge.v42.json} | 0 ...> v1beta1.panel_tests_bar_gauge2.v42.json} | 0 ....v42.json => v1beta1.candlestick.v42.json} | 0 ...beta1.canvas-connection-examples.v42.json} | 0 ...json => v1beta1.canvas-datalinks.v42.json} | 0 ....json => v1beta1.canvas-examples.v42.json} | 0 ...42.json => v1beta1.auto_decimals.v42.json} | 0 ....v42.json => v1beta1.color_modes.v42.json} | 0 ...v42.json => v1beta1.lazy_loading.v42.json} | 0 ...z.v42.json => v1beta1.linked-viz.v42.json} | 0 ... => v1beta1.panels_without_title.v42.json} | 0 ...2.json => v1beta1.shared_queries.v42.json} | 0 ...ist.v42.json => v1beta1.dashlist.v42.json} | 0 ...> v1beta1.datagrid_metric_values.v42.json} | 0 ... v1beta1.panel_tests_flame_graph.v42.json} | 0 ...on => v1beta1.gauge-multi-series.v42.json} | 0 ....v42.json => v1beta1.gauge_tests.v42.json} | 72 +-- ....json => v1beta1.gauge_tests_new.v42.json} | 0 ...> v1beta1.gauge_tests_old_to_new.v42.json} | 0 ...on => v1beta1.geomap-color-field.v42.json} | 0 ...on => v1beta1.geomap-photo-layer.v42.json} | 0 ...on => v1beta1.geomap-route-layer.v42.json} | 0 ...p-spatial-operations-transformer.v42.json} | 0 ...1.v42.json => v1beta1.geomap-v91.v42.json} | 0 ...n => v1beta1.geomap_multi-layers.v42.json} | 0 ...v42.json => v1beta1.panel-geomap.v42.json} | 0 ...1beta1.graph-gradient-area-fills.v42.json} | 0 ...=> v1beta1.graph-shared-tooltips.v42.json} | 0 ...on => v1beta1.graph-time-regions.v42.json} | 0 ....v42.json => v1beta1.graph_tests.v42.json} | 0 ...v42.json => v1beta1.graph_y_axis.v42.json} | 0 ...=> v1beta1.heatmap-calculate-log.v42.json} | 0 ...2.json => v1beta1.heatmap-legacy.v42.json} | 0 ...-x.v42.json => v1beta1.heatmap-x.v42.json} | 0 ....json => v1beta1.histogram_tests.v42.json} | 33 +- ...42.json => v1beta1.panel-library.v42.json} | 0 ...n => v1beta1.panel_test_piechart.v42.json} | 0 ...42.json => v1beta1.polystat_test.v42.json} | 0 ...json => v1beta1.panel-stat-tests.v42.json} | 0 ...atus-history-thresholds-mappings.v42.json} | 0 ...v42.json => v1beta1.table_footer.v42.json} | 0 ...on => v1beta1.table_kitchen_sink.v42.json} | 0 ...2.json => v1beta1.table_markdown.v42.json} | 0 ...json => v1beta1.table_pagination.v42.json} | 0 ... => v1beta1.table_sparkline_cell.v42.json} | 0 ....v42.json => v1beta1.table_tests.v42.json} | 0 ....json => v1beta1.table_tests_new.v42.json} | 0 ...> v1beta1.table_v12_2_migrations.v42.json} | 0 ...v42.json => v1beta1.text-options.v42.json} | 0 ...> v1beta1.timeline-align-endtime.v42.json} | 0 ...eta1.timeline-align-nulls-retain.v42.json} | 0 ...42.json => v1beta1.timeline-demo.v42.json} | 0 ...2.json => v1beta1.timeline-modes.v42.json} | 0 ...ta1.timeline-thresholds-mappings.v42.json} | 0 ...ta1.timeseries-bars-high-density.v42.json} | 0 ...imeseries-by-value-color-schemes.v42.json} | 0 ...on => v1beta1.timeseries-formats.v42.json} | 0 ...v1beta1.timeseries-gradient-area.v42.json} | 0 ...v1beta1.timeseries-hue-gradients.v42.json} | 0 ...json => v1beta1.timeseries-nulls.v42.json} | 0 ...> v1beta1.timeseries-out-of-rage.v42.json} | 0 ...s-shared-tooltip-cursor-position.v42.json} | 0 ...> v1beta1.timeseries-soft-limits.v42.json} | 0 ...n => v1beta1.timeseries-stacking.v42.json} | 0 ... => v1beta1.timeseries-stacking2.v42.json} | 0 ...=> v1beta1.timeseries-thresholds.v42.json} | 0 ....json => v1beta1.timeseries-time.v42.json} | 0 ...timeseries-y-ticks-zero-decimals.v42.json} | 0 ...> v1beta1.timeseries-yaxis-ticks.v42.json} | 0 ...s.v42.json => v1beta1.timeseries.v42.json} | 0 ...42.json => v1beta1.trend_example.v42.json} | 0 ...v42.json => v1beta1.xychart-demo.v42.json} | 0 ...on => v1beta1.xychart-migrations.v42.json} | 0 ...beta1.xychart-tooltip-color-test.v42.json} | 21 +- ...> v1beta1.mostly-blank-dashboard.v42.json} | 0 ...beta1.relative_time_zone_support.v42.json} | 0 ...ta1.slow_queries_and_annotations.v42.json} | 0 ...2.json => v1beta1.tall_dashboard.v42.json} | 0 ...son => v1beta1.time_zone_support.v42.json} | 0 ...son => v1beta1.config-from-query.v42.json} | 0 ...on => v1beta1.extract-json-paths.v42.json} | 0 ...ilter.v42.json => v1beta1.filter.v42.json} | 0 ...42.json => v1beta1.join-by-field.v42.json} | 0 ...2.json => v1beta1.join-by-labels.v42.json} | 0 ...n => v1beta1.regression-analysis.v42.json} | 0 ....reuse.v42.json => v1beta1.reuse.v42.json} | 0 ...2.json => v1beta1.rows-to-fields.v42.json} | 0 ...beta1.v1beta1.v1beta1.all-panels.v42.json} | 0 ... => v1beta1.v1beta1.v1beta1.home.v42.json} | 0 ...1.multi-lane-annotations.v42.v2alpha1.json | 6 +- ...a1.multi-lane-annotations.v42.v2beta1.json | 6 +- ...pha1.loki_query_splitting.v42.v1beta1.json | 24 +- ...estdata-nested-variables.v42.v2alpha1.json | 6 +- ...testdata-nested-variables.v42.v2beta1.json | 6 +- .../v0alpha1.heatmap-x.v42.v2alpha1.json | 9 +- .../v0alpha1.heatmap-x.v42.v2beta1.json | 9 +- .../v0alpha1.histogram_tests.v42.v1beta1.json | 33 +- ...v0alpha1.histogram_tests.v42.v2alpha1.json | 2 + .../v0alpha1.histogram_tests.v42.v2beta1.json | 2 + ...tory-thresholds-mappings.v42.v2alpha1.json | 12 +- ...story-thresholds-mappings.v42.v2beta1.json | 12 +- .../v0alpha1.table_footer.v42.v2alpha1.json | 24 +- .../v0alpha1.table_footer.v42.v2beta1.json | 24 +- ...lpha1.table_kitchen_sink.v42.v2alpha1.json | 21 +- ...alpha1.table_kitchen_sink.v42.v2beta1.json | 21 +- ...ha1.table_sparkline_cell.v42.v2alpha1.json | 17 + ...pha1.table_sparkline_cell.v42.v2beta1.json | 17 + ...1.table_v12_2_migrations.v42.v2alpha1.json | 24 +- ...a1.table_v12_2_migrations.v42.v2beta1.json | 24 +- ...line-thresholds-mappings.v42.v2alpha1.json | 12 +- ...eline-thresholds-mappings.v42.v2beta1.json | 12 +- ...timeseries-gradient-area.v42.v2alpha1.json | 18 +- ....timeseries-gradient-area.v42.v2beta1.json | 18 +- ...timeseries-hue-gradients.v42.v2alpha1.json | 27 +- ....timeseries-hue-gradients.v42.v2beta1.json | 27 +- .../v0alpha1.timeseries.v42.v2alpha1.json | 6 +- .../v0alpha1.timeseries.v42.v2beta1.json | 6 +- ...ychart-tooltip-color-test.v42.v1beta1.json | 21 +- ...chart-tooltip-color-test.v42.v2alpha1.json | 18 +- ...ychart-tooltip-color-test.v42.v2beta1.json | 18 +- .../v0alpha1.filter.v42.v2alpha1.json | 8 + .../v0alpha1.filter.v42.v2beta1.json | 8 + .../conversion/v1beta1_to_v2alpha1.go | 188 +++++++- .../conversion/v2alpha1_to_v1beta1.go | 132 ++++++ .../conversion/v2alpha1_to_v2beta1.go | 59 +++ .../loki_query_splitting.v42.json | 24 +- .../panel-gauge/gauge_tests.v42.json | 70 +-- .../panel-histogram/histogram_tests.v42.json | 33 +- .../xychart-tooltip-color-test.v42.json | 21 +- .../datasource-loki/loki_query_splitting.json | 24 +- .../panel-gauge/gauge_tests.json | 61 +-- .../panel-histogram/histogram_tests.json | 33 +- .../xychart-tooltip-color-test.json | 21 +- go.work.sum | 2 + .../dashboard.grafana.app-v2alpha1.json | 28 +- .../dashboard.grafana.app-v2beta1.json | 24 +- .../serialization/serialization-test-utils.ts | 68 +++ .../transformSaveModelV1ToV2.test.ts | 98 ++-- .../transformSaveModelV2ToV1.test.ts | 25 +- .../transformSceneToSaveModel.ts | 2 +- .../transformSceneToSaveModelSchemaV2.ts | 2 +- 202 files changed, 1501 insertions(+), 591 deletions(-) create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/annotations/v1beta1.annotation-filtering.v42.json rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/annotations/{v0alpha1.multi-lane-annotations.v42.json => v1beta1.multi-lane-annotations.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-elasticsearch/{v0alpha1.elasticsearch_complex.v42.json => v1beta1.elasticsearch_complex.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-elasticsearch/{v0alpha1.elasticsearch_migration.v42.json => v1beta1.elasticsearch_migration.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-elasticsearch/{v0alpha1.elasticsearch_simple.v42.json => v1beta1.elasticsearch_simple.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-influxdb/{v0alpha1.influxdb-logs.v42.json => v1beta1.influxdb-logs.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-influxdb/{v0alpha1.influxdb-templated.v42.json => v1beta1.influxdb-templated.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-loki/{v0alpha1.loki_fakedata.v42.json => v1beta1.loki_fakedata.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-loki/{v0alpha1.loki_query_splitting.v42.json => v1beta1.loki_query_splitting.v42.json} (98%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mssql/{v0alpha1.mssql_fakedata.v42.json => v1beta1.mssql_fakedata.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mssql/{v0alpha1.mssql_unittest.v42.json => v1beta1.mssql_unittest.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mysql/{v0alpha1.mysql_fakedata.v42.json => v1beta1.mysql_fakedata.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mysql/{v0alpha1.mysql_unittest.v42.json => v1beta1.mysql_unittest.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-opentsdb/{v0alpha1.opentsdb.v42.json => v1beta1.opentsdb.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-opentsdb/{v0alpha1.opentsdb_v23.v42.json => v1beta1.opentsdb_v23.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-postgres/{v0alpha1.postgres_fakedata.v42.json => v1beta1.postgres_fakedata.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-postgres/{v0alpha1.postgres_unittest.v42.json => v1beta1.postgres_unittest.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/{v0alpha1.bar-gauge-demo2.v42.json => v1beta1.bar-gauge-demo2.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/{v0alpha1.demo1.v42.json => v1beta1.demo1.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/{v0alpha1.new_features_in_v74.v42.json => v1beta1.new_features_in_v74.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/{v0alpha1.new_features_in_v8.v42.json => v1beta1.new_features_in_v8.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/{v0alpha1.Repeating-Kitchen-Sink.v42.json => v1beta1.Repeating-Kitchen-Sink.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/{v0alpha1.Repeating-a-panel-horizontally.v42.json => v1beta1.Repeating-a-panel-horizontally.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/{v0alpha1.Repeating-a-panel-vertically.v42.json => v1beta1.Repeating-a-panel-vertically.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/{v0alpha1.Repeating-a-row-with-a-repeating-horizontal-panel.v42.json => v1beta1.Repeating-a-row-with-a-repeating-horizontal-panel.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/{v0alpha1.Repeating-a-row-with-a-repeating-vertical-panel.v42.json => v1beta1.Repeating-a-row-with-a-repeating-vertical-panel.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/{v0alpha1.Repeating-an-empty-row.v42.json => v1beta1.Repeating-an-empty-row.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/extensions/{v0alpha1.link-onclick-extensions.v42.json => v1beta1.link-onclick-extensions.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/extensions/{v0alpha1.link-path-extensions.v42.json => v1beta1.link-path-extensions.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/{v0alpha1.datadata-macros.v42.json => v1beta1.datadata-macros.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/{v0alpha1.global-variables-and-interpolation.v42.json => v1beta1.global-variables-and-interpolation.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/{v0alpha1.templating-dashboard-links-and-variables.v42.json => v1beta1.templating-dashboard-links-and-variables.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/{v0alpha1.templating-repeating-panels.v42.json => v1beta1.templating-repeating-panels.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/{v0alpha1.templating-repeating-rows.v42.json => v1beta1.templating-repeating-rows.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/{v0alpha1.templating-textbox-e2e-scenarios.v42.json => v1beta1.templating-textbox-e2e-scenarios.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/{v0alpha1.testdata-datalinks.v42.json => v1beta1.testdata-datalinks.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/{v0alpha1.testdata-nested-variables-drilldown.v42.json => v1beta1.testdata-nested-variables-drilldown.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/{v0alpha1.testdata-nested-variables.v42.json => v1beta1.testdata-nested-variables.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/{v0alpha1.testdata-test-variable-output.v42.json => v1beta1.testdata-test-variable-output.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/{v0alpha1.testdata-variables-textbox.v42.json => v1beta1.testdata-variables-textbox.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/{v0alpha1.testdata-variables-that-update-on-time-change.v42.json => v1beta1.testdata-variables-that-update-on-time-change.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/{v0alpha1.live-flakey-refresh.v42.json => v1beta1.live-flakey-refresh.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/{v0alpha1.live-flakey.v42.json => v1beta1.live-flakey.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/{v0alpha1.live-publish.v42.json => v1beta1.live-publish.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/{v0alpha1.live-streams.v42.json => v1beta1.live-streams.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/migrations/{v0alpha1.migrations.v42.json => v1beta1.migrations.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/{v0alpha1.barchart-autosizing.v42.json => v1beta1.barchart-autosizing.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/{v0alpha1.barchart-label-rotation-skipping.v42.json => v1beta1.barchart-label-rotation-skipping.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/{v0alpha1.barchart-series-toggle.v42.json => v1beta1.barchart-series-toggle.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/{v0alpha1.barchart-thresholds-mappings.v42.json => v1beta1.barchart-thresholds-mappings.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/{v0alpha1.barchart-tooltips-legends.v42.json => v1beta1.barchart-tooltips-legends.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-bargauge/{v0alpha1.bar_gauge_demo.v42.json => v1beta1.bar_gauge_demo.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-bargauge/{v0alpha1.panel_tests_bar_gauge.v42.json => v1beta1.panel_tests_bar_gauge.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-bargauge/{v0alpha1.panel_tests_bar_gauge2.v42.json => v1beta1.panel_tests_bar_gauge2.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-candlestick/{v0alpha1.candlestick.v42.json => v1beta1.candlestick.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-canvas/{v0alpha1.canvas-connection-examples.v42.json => v1beta1.canvas-connection-examples.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-canvas/{v0alpha1.canvas-datalinks.v42.json => v1beta1.canvas-datalinks.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-canvas/{v0alpha1.canvas-examples.v42.json => v1beta1.canvas-examples.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/{v0alpha1.auto_decimals.v42.json => v1beta1.auto_decimals.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/{v0alpha1.color_modes.v42.json => v1beta1.color_modes.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/{v0alpha1.lazy_loading.v42.json => v1beta1.lazy_loading.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/{v0alpha1.linked-viz.v42.json => v1beta1.linked-viz.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/{v0alpha1.panels_without_title.v42.json => v1beta1.panels_without_title.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/{v0alpha1.shared_queries.v42.json => v1beta1.shared_queries.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-dashlist/{v0alpha1.dashlist.v42.json => v1beta1.dashlist.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-datagrid/{v0alpha1.datagrid_metric_values.v42.json => v1beta1.datagrid_metric_values.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-flamegraph/{v0alpha1.panel_tests_flame_graph.v42.json => v1beta1.panel_tests_flame_graph.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/{v0alpha1.gauge-multi-series.v42.json => v1beta1.gauge-multi-series.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/{v0alpha1.gauge_tests.v42.json => v1beta1.gauge_tests.v42.json} (93%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/{v0alpha1.gauge_tests_new.v42.json => v1beta1.gauge_tests_new.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/{v0alpha1.gauge_tests_old_to_new.v42.json => v1beta1.gauge_tests_old_to_new.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/{v0alpha1.geomap-color-field.v42.json => v1beta1.geomap-color-field.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/{v0alpha1.geomap-photo-layer.v42.json => v1beta1.geomap-photo-layer.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/{v0alpha1.geomap-route-layer.v42.json => v1beta1.geomap-route-layer.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/{v0alpha1.geomap-spatial-operations-transformer.v42.json => v1beta1.geomap-spatial-operations-transformer.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/{v0alpha1.geomap-v91.v42.json => v1beta1.geomap-v91.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/{v0alpha1.geomap_multi-layers.v42.json => v1beta1.geomap_multi-layers.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/{v0alpha1.panel-geomap.v42.json => v1beta1.panel-geomap.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/{v0alpha1.graph-gradient-area-fills.v42.json => v1beta1.graph-gradient-area-fills.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/{v0alpha1.graph-shared-tooltips.v42.json => v1beta1.graph-shared-tooltips.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/{v0alpha1.graph-time-regions.v42.json => v1beta1.graph-time-regions.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/{v0alpha1.graph_tests.v42.json => v1beta1.graph_tests.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/{v0alpha1.graph_y_axis.v42.json => v1beta1.graph_y_axis.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-heatmap/{v0alpha1.heatmap-calculate-log.v42.json => v1beta1.heatmap-calculate-log.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-heatmap/{v0alpha1.heatmap-legacy.v42.json => v1beta1.heatmap-legacy.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-heatmap/{v0alpha1.heatmap-x.v42.json => v1beta1.heatmap-x.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-histogram/{v0alpha1.histogram_tests.v42.json => v1beta1.histogram_tests.v42.json} (99%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-library/{v0alpha1.panel-library.v42.json => v1beta1.panel-library.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-piechart/{v0alpha1.panel_test_piechart.v42.json => v1beta1.panel_test_piechart.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-polystat/{v0alpha1.polystat_test.v42.json => v1beta1.polystat_test.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-stat/{v0alpha1.panel-stat-tests.v42.json => v1beta1.panel-stat-tests.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-status-history/{v0alpha1.status-history-thresholds-mappings.v42.json => v1beta1.status-history-thresholds-mappings.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/{v0alpha1.table_footer.v42.json => v1beta1.table_footer.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/{v0alpha1.table_kitchen_sink.v42.json => v1beta1.table_kitchen_sink.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/{v0alpha1.table_markdown.v42.json => v1beta1.table_markdown.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/{v0alpha1.table_pagination.v42.json => v1beta1.table_pagination.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/{v0alpha1.table_sparkline_cell.v42.json => v1beta1.table_sparkline_cell.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/{v0alpha1.table_tests.v42.json => v1beta1.table_tests.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/{v0alpha1.table_tests_new.v42.json => v1beta1.table_tests_new.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/{v0alpha1.table_v12_2_migrations.v42.json => v1beta1.table_v12_2_migrations.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-text/{v0alpha1.text-options.v42.json => v1beta1.text-options.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/{v0alpha1.timeline-align-endtime.v42.json => v1beta1.timeline-align-endtime.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/{v0alpha1.timeline-align-nulls-retain.v42.json => v1beta1.timeline-align-nulls-retain.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/{v0alpha1.timeline-demo.v42.json => v1beta1.timeline-demo.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/{v0alpha1.timeline-modes.v42.json => v1beta1.timeline-modes.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/{v0alpha1.timeline-thresholds-mappings.v42.json => v1beta1.timeline-thresholds-mappings.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/{v0alpha1.timeseries-bars-high-density.v42.json => v1beta1.timeseries-bars-high-density.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/{v0alpha1.timeseries-by-value-color-schemes.v42.json => v1beta1.timeseries-by-value-color-schemes.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/{v0alpha1.timeseries-formats.v42.json => v1beta1.timeseries-formats.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/{v0alpha1.timeseries-gradient-area.v42.json => v1beta1.timeseries-gradient-area.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/{v0alpha1.timeseries-hue-gradients.v42.json => v1beta1.timeseries-hue-gradients.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/{v0alpha1.timeseries-nulls.v42.json => v1beta1.timeseries-nulls.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/{v0alpha1.timeseries-out-of-rage.v42.json => v1beta1.timeseries-out-of-rage.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/{v0alpha1.timeseries-shared-tooltip-cursor-position.v42.json => v1beta1.timeseries-shared-tooltip-cursor-position.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/{v0alpha1.timeseries-soft-limits.v42.json => v1beta1.timeseries-soft-limits.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/{v0alpha1.timeseries-stacking.v42.json => v1beta1.timeseries-stacking.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/{v0alpha1.timeseries-stacking2.v42.json => v1beta1.timeseries-stacking2.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/{v0alpha1.timeseries-thresholds.v42.json => v1beta1.timeseries-thresholds.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/{v0alpha1.timeseries-time.v42.json => v1beta1.timeseries-time.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/{v0alpha1.timeseries-y-ticks-zero-decimals.v42.json => v1beta1.timeseries-y-ticks-zero-decimals.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/{v0alpha1.timeseries-yaxis-ticks.v42.json => v1beta1.timeseries-yaxis-ticks.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/{v0alpha1.timeseries.v42.json => v1beta1.timeseries.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-trend/{v0alpha1.trend_example.v42.json => v1beta1.trend_example.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-xychart/{v0alpha1.xychart-demo.v42.json => v1beta1.xychart-demo.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-xychart/{v0alpha1.xychart-migrations.v42.json => v1beta1.xychart-migrations.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-xychart/{v0alpha1.xychart-tooltip-color-test.v42.json => v1beta1.xychart-tooltip-color-test.v42.json} (98%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/{v0alpha1.mostly-blank-dashboard.v42.json => v1beta1.mostly-blank-dashboard.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/{v0alpha1.relative_time_zone_support.v42.json => v1beta1.relative_time_zone_support.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/{v0alpha1.slow_queries_and_annotations.v42.json => v1beta1.slow_queries_and_annotations.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/{v0alpha1.tall_dashboard.v42.json => v1beta1.tall_dashboard.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/{v0alpha1.time_zone_support.v42.json => v1beta1.time_zone_support.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/{v0alpha1.config-from-query.v42.json => v1beta1.config-from-query.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/{v0alpha1.extract-json-paths.v42.json => v1beta1.extract-json-paths.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/{v0alpha1.filter.v42.json => v1beta1.filter.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/{v0alpha1.join-by-field.v42.json => v1beta1.join-by-field.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/{v0alpha1.join-by-labels.v42.json => v1beta1.join-by-labels.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/{v0alpha1.regression-analysis.v42.json => v1beta1.regression-analysis.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/{v0alpha1.reuse.v42.json => v1beta1.reuse.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/{v0alpha1.rows-to-fields.v42.json => v1beta1.rows-to-fields.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/{v0alpha1.v1beta1.v1beta1.all-panels.v42.json => v1beta1.v1beta1.v1beta1.all-panels.v42.json} (100%) rename apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/{v0alpha1.v1beta1.v1beta1.home.v42.json => v1beta1.v1beta1.v1beta1.home.v42.json} (100%) diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index 6488de41c96..55094fe3447 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -254,8 +254,18 @@ FieldConfig: { // custom is specified by the FieldConfig field // in panel plugin schemas. custom?: {...} + + // Calculate min max per field + fieldMinMax?: bool + + // How null values should be handled when calculating field stats + // "null" - Include null values, "connected" - Ignore nulls, "null as zero" - Treat nulls as zero + nullValueMode?: NullValueMode } +// How null values should be handled +NullValueMode: "null" | "connected" | "null as zero" + DynamicConfigValue: { id: string | *"" value?: _ diff --git a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue index a8e1f121213..0802430907e 100644 --- a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue @@ -250,8 +250,18 @@ FieldConfig: { // custom is specified by the FieldConfig field // in panel plugin schemas. custom?: {...} + + // Calculate min max per field + fieldMinMax?: bool + + // How null values should be handled when calculating field stats + // "null" - Include null values, "connected" - Ignore nulls, "null as zero" - Treat nulls as zero + nullValueMode?: NullValueMode } +// How null values should be handled +NullValueMode: "null" | "connected" | "null as zero" + DynamicConfigValue: { id: string | *"" value?: _ diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue index 2b027ff98e1..293082ab82f 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @@ -258,8 +258,18 @@ FieldConfig: { // custom is specified by the FieldConfig field // in panel plugin schemas. custom?: {...} + + // Calculate min max per field + fieldMinMax?: bool + + // How null values should be handled when calculating field stats + // "null" - Include null values, "connected" - Ignore nulls, "null as zero" - Treat nulls as zero + nullValueMode?: NullValueMode } +// How null values should be handled +NullValueMode: "null" | "connected" | "null as zero" + DynamicConfigValue: { id: string | *"" value?: _ diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go index 3f594306ef5..f7ccfdd4925 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -419,6 +419,11 @@ type DashboardFieldConfig struct { // custom is specified by the FieldConfig field // in panel plugin schemas. Custom map[string]interface{} `json:"custom,omitempty"` + // Calculate min max per field + FieldMinMax *bool `json:"fieldMinMax,omitempty"` + // How null values should be handled when calculating field stats + // "null" - Include null values, "connected" - Ignore nulls, "null as zero" - Treat nulls as zero + NullValueMode *DashboardNullValueMode `json:"nullValueMode,omitempty"` } // NewDashboardFieldConfig creates a new DashboardFieldConfig object. @@ -745,6 +750,16 @@ func NewDashboardActionVariable() *DashboardActionVariable { // +k8s:openapi-gen=true const DashboardActionVariableType = "string" +// How null values should be handled +// +k8s:openapi-gen=true +type DashboardNullValueMode string + +const ( + DashboardNullValueModeNull DashboardNullValueMode = "null" + DashboardNullValueModeConnected DashboardNullValueMode = "connected" + DashboardNullValueModeNullAsZero DashboardNullValueMode = "null as zero" +) + // +k8s:openapi-gen=true type DashboardDynamicConfigValue struct { Id string `json:"id"` diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go index 4c6f3f5ed20..926d50cb49d 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go @@ -2277,6 +2277,20 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardFieldConfig(ref common.Referenc }, }, }, + "fieldMinMax": { + SchemaProps: spec.SchemaProps{ + Description: "Calculate min max per field", + Type: []string{"boolean"}, + Format: "", + }, + }, + "nullValueMode": { + SchemaProps: spec.SchemaProps{ + Description: "How null values should be handled when calculating field stats \"null\" - Include null values, \"connected\" - Ignore nulls, \"null as zero\" - Treat nulls as zero", + Type: []string{"string"}, + Format: "", + }, + }, }, }, }, diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue index 375ba67f003..41ab7bc3fa7 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue @@ -254,8 +254,18 @@ FieldConfig: { // custom is specified by the FieldConfig field // in panel plugin schemas. custom?: {...} + + // Calculate min max per field + fieldMinMax?: bool + + // How null values should be handled when calculating field stats + // "null" - Include null values, "connected" - Ignore nulls, "null as zero" - Treat nulls as zero + nullValueMode?: NullValueMode } +// How null values should be handled +NullValueMode: "null" | "connected" | "null as zero" + DynamicConfigValue: { id: string | *"" value?: _ diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go index 96054cb2fc4..06f1e1df599 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go @@ -423,6 +423,11 @@ type DashboardFieldConfig struct { // custom is specified by the FieldConfig field // in panel plugin schemas. Custom map[string]interface{} `json:"custom,omitempty"` + // Calculate min max per field + FieldMinMax *bool `json:"fieldMinMax,omitempty"` + // How null values should be handled when calculating field stats + // "null" - Include null values, "connected" - Ignore nulls, "null as zero" - Treat nulls as zero + NullValueMode *DashboardNullValueMode `json:"nullValueMode,omitempty"` } // NewDashboardFieldConfig creates a new DashboardFieldConfig object. @@ -749,6 +754,16 @@ func NewDashboardActionVariable() *DashboardActionVariable { // +k8s:openapi-gen=true const DashboardActionVariableType = "string" +// How null values should be handled +// +k8s:openapi-gen=true +type DashboardNullValueMode string + +const ( + DashboardNullValueModeNull DashboardNullValueMode = "null" + DashboardNullValueModeConnected DashboardNullValueMode = "connected" + DashboardNullValueModeNullAsZero DashboardNullValueMode = "null as zero" +) + // +k8s:openapi-gen=true type DashboardDynamicConfigValue struct { Id string `json:"id"` diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go index 402810f6e53..73c4d1f7349 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go @@ -2284,6 +2284,20 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardFieldConfig(ref common.Reference }, }, }, + "fieldMinMax": { + SchemaProps: spec.SchemaProps{ + Description: "Calculate min max per field", + Type: []string{"boolean"}, + Format: "", + }, + }, + "nullValueMode": { + SchemaProps: spec.SchemaProps{ + Description: "How null values should be handled when calculating field stats \"null\" - Include null values, \"connected\" - Ignore nulls, \"null as zero\" - Treat nulls as zero", + Type: []string{"string"}, + Format: "", + }, + }, }, }, }, diff --git a/apps/dashboard/pkg/apis/dashboard_manifest.go b/apps/dashboard/pkg/apis/dashboard_manifest.go index e94d66fec82..aee89730dc1 100644 --- a/apps/dashboard/pkg/apis/dashboard_manifest.go +++ b/apps/dashboard/pkg/apis/dashboard_manifest.go @@ -32,10 +32,10 @@ var ( rawSchemaDashboardv1beta1 = []byte(`{"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) versionSchemaDashboardv1beta1 app.VersionSchema _ = json.Unmarshal(rawSchemaDashboardv1beta1, &versionSchemaDashboardv1beta1) - rawSchemaDashboardv2alpha1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"},"valuesFormat":{"enum":["csv","json"],"type":"string"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"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","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a DataQueryKind is the datasource type","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["kind","spec"],"type":"object"},"DataSourceRef":{"additionalProperties":false,"properties":{"type":{"description":"The plugin type-id","type":"string"},"uid":{"description":"Specific datasource instance","type":"string"}},"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `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\n` + "`" + `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\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"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.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"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.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` 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.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"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.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `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.\n` + "`" + `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.\n` + "`" + `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.\n` + "`" + `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.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"regexApplyTo":{"$ref":"#/components/schemas/VariableRegexApplyTo","default":"value"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor 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.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"description":"Switch variable specification","properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing).","enum":["dontHide","hideLabel","hideVariable"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableRegexApplyTo":{"description":"Determine whether regex applies to variable value or display text\nAccepted values are ` + "`" + `value` + "`" + ` (apply to value used in queries) or ` + "`" + `text` + "`" + ` (apply to display text shown to users)","enum":["value","text"],"type":"string"},"VariableSort":{"description":"Sort variable options\nAccepted values are:\n` + "`" + `disabled` + "`" + `: No sorting\n` + "`" + `alphabeticalAsc` + "`" + `: Alphabetical ASC\n` + "`" + `alphabeticalDesc` + "`" + `: Alphabetical DESC\n` + "`" + `numericalAsc` + "`" + `: Numerical ASC\n` + "`" + `numericalDesc` + "`" + `: Numerical DESC\n` + "`" + `alphabeticalCaseInsensitiveAsc` + "`" + `: Alphabetical Case Insensitive ASC\n` + "`" + `alphabeticalCaseInsensitiveDesc` + "`" + `: Alphabetical Case Insensitive DESC\n` + "`" + `naturalAsc` + "`" + `: Natural ASC\n` + "`" + `naturalDesc` + "`" + `: Natural DESC\nVariableSort enum with default value","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a VizConfigKind is the plugin ID","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"}},"required":["kind","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"pluginVersion":{"type":"string"}},"required":["pluginVersion","options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) + rawSchemaDashboardv2alpha1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"},"valuesFormat":{"enum":["csv","json"],"type":"string"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"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","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a DataQueryKind is the datasource type","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["kind","spec"],"type":"object"},"DataSourceRef":{"additionalProperties":false,"properties":{"type":{"description":"The plugin type-id","type":"string"},"uid":{"description":"Specific datasource instance","type":"string"}},"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `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\n` + "`" + `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\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"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.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"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.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"fieldMinMax":{"description":"Calculate min max per field","type":"boolean"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"nullValueMode":{"$ref":"#/components/schemas/NullValueMode","description":"How null values should be handled when calculating field stats\n\"null\" - Include null values, \"connected\" - Ignore nulls, \"null as zero\" - Treat nulls as zero"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` 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.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"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.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `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.\n` + "`" + `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.\n` + "`" + `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.\n` + "`" + `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.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"NullValueMode":{"description":"How null values should be handled","enum":["null","connected","null as zero"],"type":"string"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"regexApplyTo":{"$ref":"#/components/schemas/VariableRegexApplyTo","default":"value"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor 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.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"description":"Switch variable specification","properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing).","enum":["dontHide","hideLabel","hideVariable"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableRegexApplyTo":{"description":"Determine whether regex applies to variable value or display text\nAccepted values are ` + "`" + `value` + "`" + ` (apply to value used in queries) or ` + "`" + `text` + "`" + ` (apply to display text shown to users)","enum":["value","text"],"type":"string"},"VariableSort":{"description":"Sort variable options\nAccepted values are:\n` + "`" + `disabled` + "`" + `: No sorting\n` + "`" + `alphabeticalAsc` + "`" + `: Alphabetical ASC\n` + "`" + `alphabeticalDesc` + "`" + `: Alphabetical DESC\n` + "`" + `numericalAsc` + "`" + `: Numerical ASC\n` + "`" + `numericalDesc` + "`" + `: Numerical DESC\n` + "`" + `alphabeticalCaseInsensitiveAsc` + "`" + `: Alphabetical Case Insensitive ASC\n` + "`" + `alphabeticalCaseInsensitiveDesc` + "`" + `: Alphabetical Case Insensitive DESC\n` + "`" + `naturalAsc` + "`" + `: Natural ASC\n` + "`" + `naturalDesc` + "`" + `: Natural DESC\nVariableSort enum with default value","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a VizConfigKind is the plugin ID","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"}},"required":["kind","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"pluginVersion":{"type":"string"}},"required":["pluginVersion","options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) versionSchemaDashboardv2alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaDashboardv2alpha1, &versionSchemaDashboardv2alpha1) - rawSchemaDashboardv2beta1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQueryPlacement":{"const":"inControlsMenu","description":"Annotation Query placement. Defines where the annotation query should be displayed.\n- \"inControlsMenu\" renders the annotation query in the dashboard controls dropdown menu","type":"string"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties. Should not be available in as code tooling.","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"placement":{"$ref":"#/components/schemas/AnnotationQueryPlacement","description":"Placement can be used to display the annotation query somewhere else on the dashboard other than the default location."},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["query","enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"},"valuesFormat":{"enum":["csv","json"],"type":"string"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"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","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"datasource":{"additionalProperties":false,"description":"New type for datasource reference\nNot creating a new type until we figure out how to handle DS refs for group by, adhoc, and every place that uses DataSourceRef in TS.","properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"DataQuery","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"version":{"default":"v0","type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `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\n` + "`" + `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\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"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.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"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.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` 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.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"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.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"const":"onTimeRangeChanged","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `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.\n` + "`" + `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.\n` + "`" + `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.\n` + "`" + `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.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"default":"A","type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeCompare":{"type":"string"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"regexApplyTo":{"$ref":"#/components/schemas/VariableRegexApplyTo","default":"value"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor 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.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing), ` + "`" + `inControlsMenu` + "`" + ` (show in a drop-down menu).","enum":["dontHide","hideLabel","hideVariable","inControlsMenu"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableRegexApplyTo":{"description":"Determine whether regex applies to variable value or display text\nAccepted values are ` + "`" + `value` + "`" + ` (apply to value used in queries) or ` + "`" + `text` + "`" + ` (apply to display text shown to users)","enum":["value","text"],"type":"string"},"VariableSort":{"description":"Sort variable options\nAccepted values are:\n` + "`" + `disabled` + "`" + `: No sorting\n` + "`" + `alphabeticalAsc` + "`" + `: Alphabetical ASC\n` + "`" + `alphabeticalDesc` + "`" + `: Alphabetical DESC\n` + "`" + `numericalAsc` + "`" + `: Numerical ASC\n` + "`" + `numericalDesc` + "`" + `: Numerical DESC\n` + "`" + `alphabeticalCaseInsensitiveAsc` + "`" + `: Alphabetical Case Insensitive ASC\n` + "`" + `alphabeticalCaseInsensitiveDesc` + "`" + `: Alphabetical Case Insensitive DESC\n` + "`" + `naturalAsc` + "`" + `: Natural ASC\n` + "`" + `naturalDesc` + "`" + `: Natural DESC\nVariableSort enum with default value","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"group":{"description":"The group is the plugin ID","type":"string"},"kind":{"const":"VizConfig","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"},"version":{"type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) + rawSchemaDashboardv2beta1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQueryPlacement":{"const":"inControlsMenu","description":"Annotation Query placement. Defines where the annotation query should be displayed.\n- \"inControlsMenu\" renders the annotation query in the dashboard controls dropdown menu","type":"string"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties. Should not be available in as code tooling.","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"placement":{"$ref":"#/components/schemas/AnnotationQueryPlacement","description":"Placement can be used to display the annotation query somewhere else on the dashboard other than the default location."},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["query","enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"},"valuesFormat":{"enum":["csv","json"],"type":"string"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"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","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"datasource":{"additionalProperties":false,"description":"New type for datasource reference\nNot creating a new type until we figure out how to handle DS refs for group by, adhoc, and every place that uses DataSourceRef in TS.","properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"DataQuery","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"version":{"default":"v0","type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `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\n` + "`" + `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\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"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.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"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.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"fieldMinMax":{"description":"Calculate min max per field","type":"boolean"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"nullValueMode":{"$ref":"#/components/schemas/NullValueMode","description":"How null values should be handled when calculating field stats\n\"null\" - Include null values, \"connected\" - Ignore nulls, \"null as zero\" - Treat nulls as zero"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` 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.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"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.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"const":"onTimeRangeChanged","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `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.\n` + "`" + `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.\n` + "`" + `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.\n` + "`" + `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.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"NullValueMode":{"description":"How null values should be handled","enum":["null","connected","null as zero"],"type":"string"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"default":"A","type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeCompare":{"type":"string"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"regexApplyTo":{"$ref":"#/components/schemas/VariableRegexApplyTo","default":"value"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor 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.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing), ` + "`" + `inControlsMenu` + "`" + ` (show in a drop-down menu).","enum":["dontHide","hideLabel","hideVariable","inControlsMenu"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableRegexApplyTo":{"description":"Determine whether regex applies to variable value or display text\nAccepted values are ` + "`" + `value` + "`" + ` (apply to value used in queries) or ` + "`" + `text` + "`" + ` (apply to display text shown to users)","enum":["value","text"],"type":"string"},"VariableSort":{"description":"Sort variable options\nAccepted values are:\n` + "`" + `disabled` + "`" + `: No sorting\n` + "`" + `alphabeticalAsc` + "`" + `: Alphabetical ASC\n` + "`" + `alphabeticalDesc` + "`" + `: Alphabetical DESC\n` + "`" + `numericalAsc` + "`" + `: Numerical ASC\n` + "`" + `numericalDesc` + "`" + `: Numerical DESC\n` + "`" + `alphabeticalCaseInsensitiveAsc` + "`" + `: Alphabetical Case Insensitive ASC\n` + "`" + `alphabeticalCaseInsensitiveDesc` + "`" + `: Alphabetical Case Insensitive DESC\n` + "`" + `naturalAsc` + "`" + `: Natural ASC\n` + "`" + `naturalDesc` + "`" + `: Natural DESC\nVariableSort enum with default value","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"group":{"description":"The group is the plugin ID","type":"string"},"kind":{"const":"VizConfig","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"},"version":{"type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) versionSchemaDashboardv2beta1 app.VersionSchema _ = json.Unmarshal(rawSchemaDashboardv2beta1, &versionSchemaDashboardv2beta1) ) diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/annotations/v1beta1.annotation-filtering.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/annotations/v1beta1.annotation-filtering.v42.json new file mode 100644 index 00000000000..5e3d12546e1 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/annotations/v1beta1.annotation-filtering.v42.json @@ -0,0 +1,427 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + }, + { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "enable": true, + "filter": { + "exclude": false, + "ids": [ + 1 + ] + }, + "iconColor": "red", + "name": "Red, only panel 1", + "target": { + "lines": 4, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "enable": true, + "filter": { + "exclude": true, + "ids": [ + 1 + ] + }, + "iconColor": "yellow", + "name": "Yellow - all except 1", + "target": { + "lines": 5, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "enable": true, + "filter": { + "exclude": false, + "ids": [ + 3, + 4 + ] + }, + "iconColor": "dark-purple", + "name": "Purple only panel 3+4", + "target": { + "lines": 6, + "refId": "Anno", + "scenarioId": "annotations" + } + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 119, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "Panel one", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "Panel two", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "Panel three", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "Panel four", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [ + "gdev", + "annotations" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Annotation filtering", + "uid": "ed155665", + "weekStart": "" +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/annotations/v1beta1.multi-lane-annotations.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/annotations/v1beta1.multi-lane-annotations.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-elasticsearch/v1beta1.elasticsearch_complex.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-elasticsearch/v1beta1.elasticsearch_complex.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_migration.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-elasticsearch/v1beta1.elasticsearch_migration.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_migration.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-elasticsearch/v1beta1.elasticsearch_migration.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_simple.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-elasticsearch/v1beta1.elasticsearch_simple.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_simple.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-elasticsearch/v1beta1.elasticsearch_simple.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-logs.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-influxdb/v1beta1.influxdb-logs.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-logs.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-influxdb/v1beta1.influxdb-logs.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-influxdb/v1beta1.influxdb-templated.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-influxdb/v1beta1.influxdb-templated.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-loki/v1beta1.loki_fakedata.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-loki/v1beta1.loki_fakedata.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_query_splitting.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-loki/v1beta1.loki_query_splitting.v42.json similarity index 98% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_query_splitting.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-loki/v1beta1.loki_query_splitting.v42.json index a7beffa4cdc..8af239195cb 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_query_splitting.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-loki/v1beta1.loki_query_splitting.v42.json @@ -219,8 +219,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -312,8 +311,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -492,8 +490,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -584,8 +581,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -676,8 +672,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -791,8 +786,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -906,8 +900,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -1022,8 +1015,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_fakedata.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mssql/v1beta1.mssql_fakedata.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_fakedata.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mssql/v1beta1.mssql_fakedata.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_unittest.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mssql/v1beta1.mssql_unittest.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mssql/v0alpha1.mssql_unittest.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mssql/v1beta1.mssql_unittest.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_fakedata.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mysql/v1beta1.mysql_fakedata.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_fakedata.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mysql/v1beta1.mysql_fakedata.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_unittest.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mysql/v1beta1.mysql_unittest.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mysql/v0alpha1.mysql_unittest.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-mysql/v1beta1.mysql_unittest.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-opentsdb/v1beta1.opentsdb.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-opentsdb/v1beta1.opentsdb.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb_v23.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-opentsdb/v1beta1.opentsdb_v23.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-opentsdb/v0alpha1.opentsdb_v23.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-opentsdb/v1beta1.opentsdb_v23.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_fakedata.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-postgres/v1beta1.postgres_fakedata.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_fakedata.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-postgres/v1beta1.postgres_fakedata.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_unittest.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-postgres/v1beta1.postgres_unittest.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-postgres/v0alpha1.postgres_unittest.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-postgres/v1beta1.postgres_unittest.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/v0alpha1.bar-gauge-demo2.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/v1beta1.bar-gauge-demo2.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/v0alpha1.bar-gauge-demo2.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/v1beta1.bar-gauge-demo2.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/v0alpha1.demo1.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/v1beta1.demo1.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/v0alpha1.demo1.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/v1beta1.demo1.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v74.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/v1beta1.new_features_in_v74.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v74.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/v1beta1.new_features_in_v74.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v8.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/v1beta1.new_features_in_v8.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/v0alpha1.new_features_in_v8.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/datasource-testdata/v1beta1.new_features_in_v8.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-Kitchen-Sink.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v1beta1.Repeating-Kitchen-Sink.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-Kitchen-Sink.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v1beta1.Repeating-Kitchen-Sink.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-horizontally.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v1beta1.Repeating-a-panel-horizontally.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-horizontally.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v1beta1.Repeating-a-panel-horizontally.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-vertically.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v1beta1.Repeating-a-panel-vertically.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-panel-vertically.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v1beta1.Repeating-a-panel-vertically.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-horizontal-panel.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v1beta1.Repeating-a-row-with-a-repeating-horizontal-panel.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-horizontal-panel.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v1beta1.Repeating-a-row-with-a-repeating-horizontal-panel.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-vertical-panel.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v1beta1.Repeating-a-row-with-a-repeating-vertical-panel.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-a-row-with-a-repeating-vertical-panel.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v1beta1.Repeating-a-row-with-a-repeating-vertical-panel.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-an-empty-row.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v1beta1.Repeating-an-empty-row.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v0alpha1.Repeating-an-empty-row.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/e2e-repeats/v1beta1.Repeating-an-empty-row.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/extensions/v0alpha1.link-onclick-extensions.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/extensions/v1beta1.link-onclick-extensions.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/extensions/v0alpha1.link-onclick-extensions.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/extensions/v1beta1.link-onclick-extensions.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/extensions/v0alpha1.link-path-extensions.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/extensions/v1beta1.link-path-extensions.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/extensions/v0alpha1.link-path-extensions.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/extensions/v1beta1.link-path-extensions.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.datadata-macros.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.datadata-macros.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.datadata-macros.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.datadata-macros.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.global-variables-and-interpolation.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.global-variables-and-interpolation.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.global-variables-and-interpolation.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.global-variables-and-interpolation.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.templating-dashboard-links-and-variables.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.templating-dashboard-links-and-variables.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.templating-dashboard-links-and-variables.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.templating-dashboard-links-and-variables.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-panels.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.templating-repeating-panels.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-panels.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.templating-repeating-panels.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-rows.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.templating-repeating-rows.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.templating-repeating-rows.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.templating-repeating-rows.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.templating-textbox-e2e-scenarios.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.templating-textbox-e2e-scenarios.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.templating-textbox-e2e-scenarios.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.templating-textbox-e2e-scenarios.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-datalinks.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.testdata-datalinks.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-datalinks.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.testdata-datalinks.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables-drilldown.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.testdata-nested-variables-drilldown.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables-drilldown.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.testdata-nested-variables-drilldown.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.testdata-nested-variables.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.testdata-nested-variables.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-test-variable-output.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.testdata-test-variable-output.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-test-variable-output.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.testdata-test-variable-output.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-textbox.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.testdata-variables-textbox.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-textbox.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.testdata-variables-textbox.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-that-update-on-time-change.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.testdata-variables-that-update-on-time-change.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-variables-that-update-on-time-change.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/feature-templating/v1beta1.testdata-variables-that-update-on-time-change.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/v0alpha1.live-flakey-refresh.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/v1beta1.live-flakey-refresh.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/v0alpha1.live-flakey-refresh.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/v1beta1.live-flakey-refresh.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/v0alpha1.live-flakey.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/v1beta1.live-flakey.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/v0alpha1.live-flakey.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/v1beta1.live-flakey.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/v0alpha1.live-publish.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/v1beta1.live-publish.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/v0alpha1.live-publish.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/v1beta1.live-publish.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/v0alpha1.live-streams.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/v1beta1.live-streams.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/v0alpha1.live-streams.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/live/v1beta1.live-streams.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/migrations/v1beta1.migrations.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/migrations/v0alpha1.migrations.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/migrations/v1beta1.migrations.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-autosizing.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v1beta1.barchart-autosizing.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-autosizing.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v1beta1.barchart-autosizing.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-label-rotation-skipping.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v1beta1.barchart-label-rotation-skipping.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-label-rotation-skipping.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v1beta1.barchart-label-rotation-skipping.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-series-toggle.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v1beta1.barchart-series-toggle.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-series-toggle.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v1beta1.barchart-series-toggle.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-thresholds-mappings.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v1beta1.barchart-thresholds-mappings.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-thresholds-mappings.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v1beta1.barchart-thresholds-mappings.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-tooltips-legends.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v1beta1.barchart-tooltips-legends.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v0alpha1.barchart-tooltips-legends.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-barchart/v1beta1.barchart-tooltips-legends.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-bargauge/v0alpha1.bar_gauge_demo.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-bargauge/v1beta1.bar_gauge_demo.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-bargauge/v0alpha1.bar_gauge_demo.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-bargauge/v1beta1.bar_gauge_demo.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-bargauge/v1beta1.panel_tests_bar_gauge.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-bargauge/v1beta1.panel_tests_bar_gauge.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge2.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-bargauge/v1beta1.panel_tests_bar_gauge2.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-bargauge/v0alpha1.panel_tests_bar_gauge2.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-bargauge/v1beta1.panel_tests_bar_gauge2.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-candlestick/v0alpha1.candlestick.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-candlestick/v1beta1.candlestick.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-candlestick/v0alpha1.candlestick.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-candlestick/v1beta1.candlestick.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-connection-examples.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-canvas/v1beta1.canvas-connection-examples.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-connection-examples.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-canvas/v1beta1.canvas-connection-examples.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-datalinks.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-canvas/v1beta1.canvas-datalinks.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-datalinks.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-canvas/v1beta1.canvas-datalinks.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-examples.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-canvas/v1beta1.canvas-examples.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-canvas/v0alpha1.canvas-examples.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-canvas/v1beta1.canvas-examples.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v0alpha1.auto_decimals.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v1beta1.auto_decimals.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v0alpha1.auto_decimals.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v1beta1.auto_decimals.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v0alpha1.color_modes.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v1beta1.color_modes.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v0alpha1.color_modes.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v1beta1.color_modes.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v0alpha1.lazy_loading.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v1beta1.lazy_loading.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v0alpha1.lazy_loading.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v1beta1.lazy_loading.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v0alpha1.linked-viz.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v1beta1.linked-viz.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v0alpha1.linked-viz.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v1beta1.linked-viz.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v0alpha1.panels_without_title.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v1beta1.panels_without_title.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v0alpha1.panels_without_title.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v1beta1.panels_without_title.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v0alpha1.shared_queries.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v1beta1.shared_queries.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v0alpha1.shared_queries.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-common/v1beta1.shared_queries.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-dashlist/v0alpha1.dashlist.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-dashlist/v1beta1.dashlist.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-dashlist/v0alpha1.dashlist.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-dashlist/v1beta1.dashlist.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-datagrid/v0alpha1.datagrid_metric_values.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-datagrid/v1beta1.datagrid_metric_values.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-datagrid/v0alpha1.datagrid_metric_values.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-datagrid/v1beta1.datagrid_metric_values.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-flamegraph/v0alpha1.panel_tests_flame_graph.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-flamegraph/v1beta1.panel_tests_flame_graph.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-flamegraph/v0alpha1.panel_tests_flame_graph.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-flamegraph/v1beta1.panel_tests_flame_graph.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge-multi-series.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v1beta1.gauge-multi-series.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge-multi-series.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v1beta1.gauge-multi-series.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v1beta1.gauge_tests.v42.json similarity index 93% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v1beta1.gauge_tests.v42.json index 92a865b0b10..b00b08dd2ab 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v1beta1.gauge_tests.v42.json @@ -65,17 +65,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -136,17 +133,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -207,17 +201,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -271,7 +262,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [], "max": 100, "min": 0, @@ -279,17 +269,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -342,7 +329,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [], "max": 100, "min": 0, @@ -350,17 +336,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -414,7 +397,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [], "max": 100, "min": 0, @@ -422,17 +404,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -485,7 +464,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [], "max": 100, "min": 0, @@ -493,17 +471,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -668,7 +643,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [ { "options": { @@ -685,17 +659,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { - "color": "#e24d42", - "index": 2, + "color": "#e24d42", "value": 90 } ] @@ -750,7 +721,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [ { "options": { @@ -768,17 +738,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -833,7 +800,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [ { "options": { @@ -852,17 +818,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -917,7 +880,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [ { "options": { @@ -946,17 +908,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -1038,7 +997,7 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "2", + "decimals": 2, "mappings": [], "max": 100, "min": 0, @@ -1046,17 +1005,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v1beta1.gauge_tests_new.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v1beta1.gauge_tests_new.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v1beta1.gauge_tests_old_to_new.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v1beta1.gauge_tests_old_to_new.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-color-field.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v1beta1.geomap-color-field.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-color-field.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v1beta1.geomap-color-field.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-photo-layer.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v1beta1.geomap-photo-layer.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-photo-layer.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v1beta1.geomap-photo-layer.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-route-layer.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v1beta1.geomap-route-layer.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-route-layer.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v1beta1.geomap-route-layer.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-spatial-operations-transformer.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v1beta1.geomap-spatial-operations-transformer.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-spatial-operations-transformer.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v1beta1.geomap-spatial-operations-transformer.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-v91.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v1beta1.geomap-v91.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap-v91.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v1beta1.geomap-v91.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap_multi-layers.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v1beta1.geomap_multi-layers.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v0alpha1.geomap_multi-layers.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v1beta1.geomap_multi-layers.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v0alpha1.panel-geomap.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v1beta1.panel-geomap.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v0alpha1.panel-geomap.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-geomap/v1beta1.panel-geomap.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v0alpha1.graph-gradient-area-fills.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v1beta1.graph-gradient-area-fills.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v0alpha1.graph-gradient-area-fills.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v1beta1.graph-gradient-area-fills.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v0alpha1.graph-shared-tooltips.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v1beta1.graph-shared-tooltips.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v0alpha1.graph-shared-tooltips.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v1beta1.graph-shared-tooltips.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v0alpha1.graph-time-regions.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v1beta1.graph-time-regions.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v0alpha1.graph-time-regions.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v1beta1.graph-time-regions.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v0alpha1.graph_tests.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v1beta1.graph_tests.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v0alpha1.graph_tests.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v1beta1.graph_tests.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v0alpha1.graph_y_axis.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v1beta1.graph_y_axis.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v0alpha1.graph_y_axis.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-graph/v1beta1.graph_y_axis.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-calculate-log.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-heatmap/v1beta1.heatmap-calculate-log.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-calculate-log.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-heatmap/v1beta1.heatmap-calculate-log.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-legacy.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-heatmap/v1beta1.heatmap-legacy.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-legacy.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-heatmap/v1beta1.heatmap-legacy.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-heatmap/v1beta1.heatmap-x.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-heatmap/v1beta1.heatmap-x.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-histogram/v1beta1.histogram_tests.v42.json similarity index 99% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-histogram/v1beta1.histogram_tests.v42.json index 7e392bd55d0..5b3e0ed0b72 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-histogram/v1beta1.histogram_tests.v42.json @@ -58,8 +58,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -127,8 +126,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -196,8 +194,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -277,8 +274,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -355,8 +351,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -448,8 +443,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -536,8 +530,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [ { @@ -619,8 +612,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [ { @@ -702,8 +694,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [ { @@ -785,8 +776,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -850,8 +840,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-library/v0alpha1.panel-library.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-library/v1beta1.panel-library.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-library/v0alpha1.panel-library.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-library/v1beta1.panel-library.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-piechart/v0alpha1.panel_test_piechart.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-piechart/v1beta1.panel_test_piechart.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-piechart/v0alpha1.panel_test_piechart.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-piechart/v1beta1.panel_test_piechart.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-polystat/v0alpha1.polystat_test.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-polystat/v1beta1.polystat_test.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-polystat/v0alpha1.polystat_test.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-polystat/v1beta1.polystat_test.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-stat/v0alpha1.panel-stat-tests.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-stat/v1beta1.panel-stat-tests.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-stat/v0alpha1.panel-stat-tests.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-stat/v1beta1.panel-stat-tests.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-status-history/v1beta1.status-history-thresholds-mappings.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-status-history/v1beta1.status-history-thresholds-mappings.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v1beta1.table_footer.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v1beta1.table_footer.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v1beta1.table_kitchen_sink.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v1beta1.table_kitchen_sink.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v0alpha1.table_markdown.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v1beta1.table_markdown.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v0alpha1.table_markdown.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v1beta1.table_markdown.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v0alpha1.table_pagination.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v1beta1.table_pagination.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v0alpha1.table_pagination.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v1beta1.table_pagination.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v1beta1.table_sparkline_cell.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v1beta1.table_sparkline_cell.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v0alpha1.table_tests.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v1beta1.table_tests.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v0alpha1.table_tests.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v1beta1.table_tests.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v0alpha1.table_tests_new.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v1beta1.table_tests_new.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v0alpha1.table_tests_new.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v1beta1.table_tests_new.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v1beta1.table_v12_2_migrations.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-table/v1beta1.table_v12_2_migrations.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-text/v0alpha1.text-options.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-text/v1beta1.text-options.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-text/v0alpha1.text-options.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-text/v1beta1.text-options.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-endtime.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v1beta1.timeline-align-endtime.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-endtime.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v1beta1.timeline-align-endtime.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-nulls-retain.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v1beta1.timeline-align-nulls-retain.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-align-nulls-retain.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v1beta1.timeline-align-nulls-retain.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-demo.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v1beta1.timeline-demo.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-demo.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v1beta1.timeline-demo.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-modes.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v1beta1.timeline-modes.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-modes.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v1beta1.timeline-modes.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v1beta1.timeline-thresholds-mappings.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeline/v1beta1.timeline-thresholds-mappings.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-bars-high-density.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-bars-high-density.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-bars-high-density.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-bars-high-density.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-by-value-color-schemes.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-by-value-color-schemes.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-by-value-color-schemes.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-by-value-color-schemes.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-formats.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-formats.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-formats.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-formats.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-gradient-area.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-gradient-area.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-hue-gradients.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-hue-gradients.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-nulls.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-nulls.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-nulls.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-nulls.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-out-of-rage.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-out-of-rage.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-out-of-rage.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-out-of-rage.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-shared-tooltip-cursor-position.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-shared-tooltip-cursor-position.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-shared-tooltip-cursor-position.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-shared-tooltip-cursor-position.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-soft-limits.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-soft-limits.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-soft-limits.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-soft-limits.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-stacking.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-stacking.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking2.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-stacking2.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-stacking2.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-stacking2.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-thresholds.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-thresholds.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-thresholds.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-thresholds.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-time.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-time.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-time.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-time.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-y-ticks-zero-decimals.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-y-ticks-zero-decimals.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-y-ticks-zero-decimals.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-y-ticks-zero-decimals.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-yaxis-ticks.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-yaxis-ticks.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-yaxis-ticks.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries-yaxis-ticks.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-timeseries/v1beta1.timeseries.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-trend/v0alpha1.trend_example.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-trend/v1beta1.trend_example.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-trend/v0alpha1.trend_example.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-trend/v1beta1.trend_example.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-demo.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-xychart/v1beta1.xychart-demo.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-demo.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-xychart/v1beta1.xychart-demo.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-migrations.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-xychart/v1beta1.xychart-migrations.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-migrations.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-xychart/v1beta1.xychart-migrations.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-xychart/v1beta1.xychart-tooltip-color-test.v42.json similarity index 98% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-xychart/v1beta1.xychart-tooltip-color-test.v42.json index 417ea1661e1..f28fee864e5 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-xychart/v1beta1.xychart-tooltip-color-test.v42.json @@ -61,8 +61,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -148,8 +147,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -235,8 +233,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -322,8 +319,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -416,8 +412,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -510,8 +505,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -604,8 +598,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v0alpha1.mostly-blank-dashboard.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v1beta1.mostly-blank-dashboard.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v0alpha1.mostly-blank-dashboard.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v1beta1.mostly-blank-dashboard.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v0alpha1.relative_time_zone_support.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v1beta1.relative_time_zone_support.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v0alpha1.relative_time_zone_support.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v1beta1.relative_time_zone_support.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v0alpha1.slow_queries_and_annotations.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v1beta1.slow_queries_and_annotations.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v0alpha1.slow_queries_and_annotations.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v1beta1.slow_queries_and_annotations.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v0alpha1.tall_dashboard.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v1beta1.tall_dashboard.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v0alpha1.tall_dashboard.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v1beta1.tall_dashboard.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v0alpha1.time_zone_support.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v1beta1.time_zone_support.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v0alpha1.time_zone_support.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/scenarios/v1beta1.time_zone_support.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v0alpha1.config-from-query.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v1beta1.config-from-query.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v0alpha1.config-from-query.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v1beta1.config-from-query.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v0alpha1.extract-json-paths.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v1beta1.extract-json-paths.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v0alpha1.extract-json-paths.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v1beta1.extract-json-paths.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v1beta1.filter.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v1beta1.filter.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v0alpha1.join-by-field.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v1beta1.join-by-field.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v0alpha1.join-by-field.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v1beta1.join-by-field.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v0alpha1.join-by-labels.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v1beta1.join-by-labels.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v0alpha1.join-by-labels.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v1beta1.join-by-labels.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v0alpha1.regression-analysis.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v1beta1.regression-analysis.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v0alpha1.regression-analysis.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v1beta1.regression-analysis.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v0alpha1.reuse.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v1beta1.reuse.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v0alpha1.reuse.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v1beta1.reuse.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v0alpha1.rows-to-fields.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v1beta1.rows-to-fields.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v0alpha1.rows-to-fields.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/transforms/v1beta1.rows-to-fields.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.all-panels.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/v1beta1.v1beta1.v1beta1.all-panels.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.all-panels.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/v1beta1.v1beta1.v1beta1.all-panels.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.home.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/v1beta1.v1beta1.v1beta1.home.v42.json similarity index 100% rename from apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/v0alpha1.v1beta1.v1beta1.home.v42.json rename to apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/v1beta1.v1beta1.v1beta1.home.v42.json diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.v2alpha1.json index b6addcc81ed..d6f207c6fdd 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.v2alpha1.json @@ -970,8 +970,7 @@ "le": 1e-9 }, "legend": { - "show": true, - "showLegend": true + "show": true }, "rowsFrame": { "layout": "auto" @@ -1064,8 +1063,7 @@ "le": 1e-9 }, "legend": { - "show": true, - "showLegend": true + "show": true }, "rowsFrame": { "layout": "auto" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.v2beta1.json index f806e27a98f..c7ef28fa2b8 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/annotations/v0alpha1.multi-lane-annotations.v42.v2beta1.json @@ -991,8 +991,7 @@ "le": 1e-9 }, "legend": { - "show": true, - "showLegend": true + "show": true }, "rowsFrame": { "layout": "auto" @@ -1087,8 +1086,7 @@ "le": 1e-9 }, "legend": { - "show": true, - "showLegend": true + "show": true }, "rowsFrame": { "layout": "auto" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_query_splitting.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_query_splitting.v42.v1beta1.json index aed8292522f..3cb966ba891 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_query_splitting.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_query_splitting.v42.v1beta1.json @@ -225,8 +225,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -318,8 +317,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -498,8 +496,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -590,8 +587,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -682,8 +678,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -797,8 +792,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -912,8 +906,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -1028,8 +1021,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.v2alpha1.json index b1dbd3de041..4d208a1d8dc 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.v2alpha1.json @@ -467,7 +467,8 @@ "title": "Go to drilldown", "url": "/d/O6GmNPvWk/dashboard-tests-nested-template-variables-drilldown?orgId=1\u0026${__all_variables}\u0026${__url_time_range}" } - ] + ], + "nullValueMode": "connected" }, "overrides": [] } @@ -550,7 +551,8 @@ "title": "Go to drilldown", "url": "/d/O6GmNPvWk/dashboard-tests-nested-template-variables-drilldown?orgId=1\u0026${__all_variables}\u0026${__url_time_range}" } - ] + ], + "nullValueMode": "connected" }, "overrides": [] } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.v2beta1.json index 9089dd1d1fb..5165f97554d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/feature-templating/v0alpha1.testdata-nested-variables.v42.v2beta1.json @@ -481,7 +481,8 @@ "title": "Go to drilldown", "url": "/d/O6GmNPvWk/dashboard-tests-nested-template-variables-drilldown?orgId=1\u0026${__all_variables}\u0026${__url_time_range}" } - ] + ], + "nullValueMode": "connected" }, "overrides": [] } @@ -566,7 +567,8 @@ "title": "Go to drilldown", "url": "/d/O6GmNPvWk/dashboard-tests-nested-template-variables-drilldown?orgId=1\u0026${__all_variables}\u0026${__url_time_range}" } - ] + ], + "nullValueMode": "connected" }, "overrides": [] } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.v2alpha1.json index 2eb67e36f2f..a1b9c4b230a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.v2alpha1.json @@ -169,8 +169,7 @@ "le": 1e-9 }, "legend": { - "show": true, - "showLegend": true + "show": true }, "rowsFrame": { "layout": "auto" @@ -336,8 +335,7 @@ "le": 1e-9 }, "legend": { - "show": true, - "showLegend": true + "show": true }, "rowsFrame": { "layout": "auto" @@ -408,8 +406,7 @@ "le": 1e-9 }, "legend": { - "show": true, - "showLegend": true + "show": true }, "rowsFrame": { "layout": "auto" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.v2beta1.json index acba4cedbc2..2428d6fd107 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-heatmap/v0alpha1.heatmap-x.v42.v2beta1.json @@ -175,8 +175,7 @@ "le": 1e-9 }, "legend": { - "show": true, - "showLegend": true + "show": true }, "rowsFrame": { "layout": "auto" @@ -347,8 +346,7 @@ "le": 1e-9 }, "legend": { - "show": true, - "showLegend": true + "show": true }, "rowsFrame": { "layout": "auto" @@ -420,8 +418,7 @@ "le": 1e-9 }, "legend": { - "show": true, - "showLegend": true + "show": true }, "rowsFrame": { "layout": "auto" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v1beta1.json index b65608bc758..c3435f0f17d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v1beta1.json @@ -64,8 +64,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -133,8 +132,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -202,8 +200,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -283,8 +280,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -361,8 +357,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -454,8 +449,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -542,8 +536,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [ { @@ -625,8 +618,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [ { @@ -708,8 +700,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [ { @@ -791,8 +782,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -856,8 +846,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v2alpha1.json index 57fbcad9d99..ec5c52a7119 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v2alpha1.json @@ -882,6 +882,7 @@ "kind": "filterFieldsByName", "spec": { "id": "filterFieldsByName", + "disabled": true, "options": { "include": { "names": [ @@ -895,6 +896,7 @@ "kind": "histogram", "spec": { "id": "histogram", + "disabled": true, "options": { "combine": true, "fields": {} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v2beta1.json index 5b2ee8d8df2..3ff41469dbc 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-histogram/v0alpha1.histogram_tests.v42.v2beta1.json @@ -911,6 +911,7 @@ "kind": "filterFieldsByName", "spec": { "id": "filterFieldsByName", + "disabled": true, "options": { "include": { "names": [ @@ -924,6 +925,7 @@ "kind": "histogram", "spec": { "id": "histogram", + "disabled": true, "options": { "combine": true, "fields": {} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.v2alpha1.json index d90c8dc52cd..b9ba0b13da4 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.v2alpha1.json @@ -222,7 +222,8 @@ "insertNulls": false, "lineWidth": 0, "spanNulls": false - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -318,7 +319,8 @@ "insertNulls": false, "lineWidth": 0, "spanNulls": false - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -424,7 +426,8 @@ "insertNulls": false, "lineWidth": 0, "spanNulls": false - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -507,7 +510,8 @@ "insertNulls": false, "lineWidth": 0, "spanNulls": false - } + }, + "fieldMinMax": false }, "overrides": [] } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.v2beta1.json index 7aaa0fff33a..e130fc7e172 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-status-history/v0alpha1.status-history-thresholds-mappings.v42.v2beta1.json @@ -229,7 +229,8 @@ "insertNulls": false, "lineWidth": 0, "spanNulls": false - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -328,7 +329,8 @@ "insertNulls": false, "lineWidth": 0, "spanNulls": false - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -437,7 +439,8 @@ "insertNulls": false, "lineWidth": 0, "spanNulls": false - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -523,7 +526,8 @@ "insertNulls": false, "lineWidth": 0, "spanNulls": false - } + }, + "fieldMinMax": false }, "overrides": [] } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2alpha1.json index 540f0d9e54d..9d15475c82d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2alpha1.json @@ -167,7 +167,8 @@ }, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -361,7 +362,8 @@ }, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -596,7 +598,8 @@ }, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -787,7 +790,8 @@ }, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -974,7 +978,8 @@ }, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1181,7 +1186,8 @@ }, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1384,7 +1390,8 @@ }, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1573,7 +1580,8 @@ }, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2beta1.json index 6c9aa023163..3965312c00a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2beta1.json @@ -173,7 +173,8 @@ }, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -372,7 +373,8 @@ }, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -612,7 +614,8 @@ }, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -808,7 +811,8 @@ }, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1000,7 +1004,8 @@ }, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1212,7 +1217,8 @@ }, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1420,7 +1426,8 @@ }, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1614,7 +1621,8 @@ }, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2alpha1.json index bccce10d162..f342cab8373 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2alpha1.json @@ -194,7 +194,8 @@ "filterable": true, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1190,7 +1191,8 @@ "reducer": [] }, "inspect": true - } + }, + "fieldMinMax": true }, "overrides": [] } @@ -1262,7 +1264,8 @@ "type": "auto" }, "inspect": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1418,7 +1421,8 @@ "type": "auto" }, "inspect": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1575,7 +1579,8 @@ "type": "auto" }, "inspect": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1737,7 +1742,8 @@ "type": "auto" }, "inspect": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1888,7 +1894,8 @@ "type": "auto" }, "inspect": false - } + }, + "fieldMinMax": true }, "overrides": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2beta1.json index 5e186ef1443..59f9b3d7942 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2beta1.json @@ -200,7 +200,8 @@ "filterable": true, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1208,7 +1209,8 @@ "reducer": [] }, "inspect": true - } + }, + "fieldMinMax": true }, "overrides": [] } @@ -1283,7 +1285,8 @@ "type": "auto" }, "inspect": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1442,7 +1445,8 @@ "type": "auto" }, "inspect": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1602,7 +1606,8 @@ "type": "auto" }, "inspect": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1767,7 +1772,8 @@ "type": "auto" }, "inspect": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1921,7 +1927,8 @@ "type": "auto" }, "inspect": false - } + }, + "fieldMinMax": true }, "overrides": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.v2alpha1.json index e5e260fd150..92729fdddcb 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.v2alpha1.json @@ -302,6 +302,23 @@ "url": "https://google.com/search?q=grafana" } ], + "actions": [ + { + "type": "fetch", + "title": "Get instance health", + "fetch": { + "method": "GET", + "url": "/api/health", + "body": "{}", + "headers": [ + [ + "Content-Type", + "application/json" + ] + ] + } + } + ], "custom": { "align": "auto", "cellOptions": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.v2beta1.json index ac15a298939..5246af0a06b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_sparkline_cell.v42.v2beta1.json @@ -312,6 +312,23 @@ "url": "https://google.com/search?q=grafana" } ], + "actions": [ + { + "type": "fetch", + "title": "Get instance health", + "fetch": { + "method": "GET", + "url": "/api/health", + "body": "{}", + "headers": [ + [ + "Content-Type", + "application/json" + ] + ] + } + } + ], "custom": { "align": "auto", "cellOptions": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2alpha1.json index 1b6348c35d5..d6451b5d80f 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2alpha1.json @@ -206,7 +206,8 @@ }, "filterable": true, "inspect": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -753,7 +754,8 @@ }, "filterable": true, "inspect": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1303,7 +1305,8 @@ "filterable": true, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1497,7 +1500,8 @@ "filterable": true, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1692,7 +1696,8 @@ "filterable": true, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1886,7 +1891,8 @@ "filterable": true, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -2081,7 +2087,8 @@ "filterable": true, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -2276,7 +2283,8 @@ "filterable": true, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2beta1.json index a77140c5beb..75353a995ac 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2beta1.json @@ -212,7 +212,8 @@ }, "filterable": true, "inspect": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -764,7 +765,8 @@ }, "filterable": true, "inspect": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1319,7 +1321,8 @@ "filterable": true, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1518,7 +1521,8 @@ "filterable": true, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1718,7 +1722,8 @@ "filterable": true, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -1917,7 +1922,8 @@ "filterable": true, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -2117,7 +2123,8 @@ "filterable": true, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { @@ -2317,7 +2324,8 @@ "filterable": true, "inspect": false, "wrapText": false - } + }, + "fieldMinMax": true }, "overrides": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.v2alpha1.json index 8dff3c34ccf..3366490c00a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.v2alpha1.json @@ -222,7 +222,8 @@ "insertNulls": false, "lineWidth": 0, "spanNulls": false - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -318,7 +319,8 @@ "insertNulls": false, "lineWidth": 0, "spanNulls": false - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -424,7 +426,8 @@ "insertNulls": false, "lineWidth": 0, "spanNulls": false - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -507,7 +510,8 @@ "insertNulls": false, "lineWidth": 0, "spanNulls": false - } + }, + "fieldMinMax": false }, "overrides": [] } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.v2beta1.json index 3baa3d21130..59d2e929972 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeline/v0alpha1.timeline-thresholds-mappings.v42.v2beta1.json @@ -229,7 +229,8 @@ "insertNulls": false, "lineWidth": 0, "spanNulls": false - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -328,7 +329,8 @@ "insertNulls": false, "lineWidth": 0, "spanNulls": false - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -437,7 +439,8 @@ "insertNulls": false, "lineWidth": 0, "spanNulls": false - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -523,7 +526,8 @@ "insertNulls": false, "lineWidth": 0, "spanNulls": false - } + }, + "fieldMinMax": false }, "overrides": [] } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.v2alpha1.json index 5b43876c65f..bde73320d42 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.v2alpha1.json @@ -110,7 +110,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -217,7 +218,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -324,7 +326,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -431,7 +434,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -537,7 +541,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -643,7 +648,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.v2beta1.json index ca96f9d5720..06331f32233 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-gradient-area.v42.v2beta1.json @@ -114,7 +114,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -223,7 +224,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -332,7 +334,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -441,7 +444,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -549,7 +553,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -657,7 +662,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.v2alpha1.json index 74cba148009..c4bb5720d36 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.v2alpha1.json @@ -116,7 +116,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -229,7 +230,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -342,7 +344,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -455,7 +458,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -568,7 +572,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -681,7 +686,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -794,7 +800,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -907,7 +914,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -1020,7 +1028,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.v2beta1.json index 7e64bc79ef3..7c18be27f07 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries-hue-gradients.v42.v2beta1.json @@ -120,7 +120,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -235,7 +236,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -350,7 +352,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -465,7 +468,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -580,7 +584,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -695,7 +700,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -810,7 +816,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -925,7 +932,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -1040,7 +1048,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.v2alpha1.json index e50b453076a..cb63e4f234d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.v2alpha1.json @@ -3607,7 +3607,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -3740,7 +3741,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.v2beta1.json index 65105663c85..a57e430cc63 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-timeseries/v0alpha1.timeseries.v42.v2beta1.json @@ -3674,7 +3674,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { @@ -3809,7 +3810,8 @@ }, "showPoints": "never", "spanNulls": true - } + }, + "nullValueMode": "null" }, "overrides": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v1beta1.json index cdc93bf3cfa..1b7c9effbe8 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v1beta1.json @@ -67,8 +67,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -154,8 +153,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -241,8 +239,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -328,8 +325,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -422,8 +418,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -516,8 +511,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -610,8 +604,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v2alpha1.json index 1d60f0ef9bf..861c4b41a6c 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v2alpha1.json @@ -124,7 +124,8 @@ "type": "linear" }, "show": "points" - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -225,7 +226,8 @@ "type": "linear" }, "show": "points" - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -326,7 +328,8 @@ "type": "linear" }, "show": "points" - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -434,7 +437,8 @@ "type": "linear" }, "show": "points" - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -542,7 +546,8 @@ "type": "linear" }, "show": "points" - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -650,7 +655,8 @@ "type": "linear" }, "show": "points" - } + }, + "fieldMinMax": false }, "overrides": [] } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v2beta1.json index 5a46646474d..0ae6dc172bd 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-xychart/v0alpha1.xychart-tooltip-color-test.v42.v2beta1.json @@ -128,7 +128,8 @@ "type": "linear" }, "show": "points" - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -232,7 +233,8 @@ "type": "linear" }, "show": "points" - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -336,7 +338,8 @@ "type": "linear" }, "show": "points" - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -447,7 +450,8 @@ "type": "linear" }, "show": "points" - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -558,7 +562,8 @@ "type": "linear" }, "show": "points" - } + }, + "fieldMinMax": false }, "overrides": [] } @@ -669,7 +674,8 @@ "type": "linear" }, "show": "points" - } + }, + "fieldMinMax": false }, "overrides": [] } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.v2alpha1.json index 056fdc62383..dd3ba7146e5 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.v2alpha1.json @@ -81,6 +81,10 @@ "kind": "reduce", "spec": { "id": "reduce", + "filter": { + "id": "byRefId", + "options": "A" + }, "options": { "includeTimeField": false, "mode": "reduceFields", @@ -94,6 +98,10 @@ "kind": "reduce", "spec": { "id": "reduce", + "filter": { + "id": "byRefId", + "options": "B" + }, "options": { "includeTimeField": false, "mode": "reduceFields", diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.v2beta1.json index 57c5559add1..0f4ab69c96a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/transforms/v0alpha1.filter.v42.v2beta1.json @@ -86,6 +86,10 @@ "kind": "reduce", "spec": { "id": "reduce", + "filter": { + "id": "byRefId", + "options": "A" + }, "options": { "includeTimeField": false, "mode": "reduceFields", @@ -99,6 +103,10 @@ "kind": "reduce", "spec": { "id": "reduce", + "filter": { + "id": "byRefId", + "options": "B" + }, "options": { "includeTimeField": false, "mode": "reduceFields", diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go index 3eeb61893fa..bfff3c49797 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go @@ -2230,6 +2230,20 @@ func transformPanelTransformations(panelMap map[string]interface{}) []dashv2alph Options: options, }, } + + // Extract disabled if present (optional, transformations are enabled by default) + if disabled, ok := tMap["disabled"].(bool); ok && disabled { + transformationKind.Spec.Disabled = &disabled + } + + // Extract filter if present (optional frame matcher for transformations) + if filterMap, ok := tMap["filter"].(map[string]interface{}); ok { + transformationKind.Spec.Filter = &dashv2alpha1.DashboardMatcherConfig{ + Id: schemaversion.GetStringValue(filterMap, "id"), + Options: filterMap["options"], + } + } + result = append(result, transformationKind) } } @@ -2349,14 +2363,6 @@ func buildVizConfig(panelMap map[string]interface{}) dashv2alpha1.DashboardVizCo } } - // Add frontend-style default options to match frontend behavior - if legend, ok := options["legend"].(map[string]interface{}); ok { - // Add showLegend: true to match frontend behavior - showLegend := getBoolField(legend, "showLegend", true) - legend["showLegend"] = showLegend - options["legend"] = legend - } - // Handle Angular panel migrations // This replicates the v0→v1 migration logic for panels that weren't migrated yet. // We check two cases: @@ -2531,6 +2537,15 @@ func extractFieldConfigDefaults(defaults map[string]interface{}) dashv2alpha1.Da fieldConfigDefaults.Writeable = val hasDefaults = true } + if val, ok := extractBoolField(defaults, "fieldMinMax"); ok { + fieldConfigDefaults.FieldMinMax = val + hasDefaults = true + } + if val, ok := defaults["nullValueMode"].(string); ok { + nullValueMode := dashv2alpha1.DashboardNullValueMode(val) + fieldConfigDefaults.NullValueMode = &nullValueMode + hasDefaults = true + } // Extract array field - strip BOMs from link URLs if linksArray, ok := extractArrayField(defaults, "links"); ok { @@ -2543,6 +2558,12 @@ func extractFieldConfigDefaults(defaults map[string]interface{}) dashv2alpha1.Da hasDefaults = true } + // Extract actions array + if actionsArray, ok := extractArrayField(defaults, "actions"); ok { + fieldConfigDefaults.Actions = convertActionsToV2(actionsArray) + hasDefaults = true + } + // Extract mappings if mappings, exists := defaults["mappings"]; exists { resultMappings := buildValueMappings(mappings) @@ -2842,6 +2863,157 @@ func extractFieldConfigOverrides(fieldConfig map[string]interface{}) []dashv2alp return result } +// convertActionsToV2 converts an array of V1 action objects to V2 DashboardAction structs. +func convertActionsToV2(actionsArray []interface{}) []dashv2alpha1.DashboardAction { + if len(actionsArray) == 0 { + return nil + } + + result := make([]dashv2alpha1.DashboardAction, 0, len(actionsArray)) + for _, action := range actionsArray { + actionMap, ok := action.(map[string]interface{}) + if !ok { + continue + } + + dashAction := dashv2alpha1.DashboardAction{ + Type: dashv2alpha1.DashboardActionType(schemaversion.GetStringValue(actionMap, "type")), + Title: schemaversion.GetStringValue(actionMap, "title"), + } + + // Convert confirmation + if confirmation, ok := actionMap["confirmation"].(string); ok && confirmation != "" { + dashAction.Confirmation = &confirmation + } + + // Convert oneClick + if oneClick, ok := actionMap["oneClick"].(bool); ok { + dashAction.OneClick = &oneClick + } + + // Convert fetch options + if fetchMap, ok := actionMap["fetch"].(map[string]interface{}); ok { + dashAction.Fetch = convertFetchOptionsToV2(fetchMap) + } + + // Convert infinity options + if infinityMap, ok := actionMap["infinity"].(map[string]interface{}); ok { + dashAction.Infinity = convertInfinityOptionsToV2(infinityMap) + } + + // Convert variables + if variablesArray, ok := actionMap["variables"].([]interface{}); ok { + dashAction.Variables = convertActionVariablesToV2(variablesArray) + } + + // Convert style + if styleMap, ok := actionMap["style"].(map[string]interface{}); ok { + dashAction.Style = convertActionStyleToV2(styleMap) + } + + result = append(result, dashAction) + } + + return result +} + +func convertFetchOptionsToV2(fetchMap map[string]interface{}) *dashv2alpha1.DashboardFetchOptions { + fetchOptions := &dashv2alpha1.DashboardFetchOptions{ + Method: dashv2alpha1.DashboardHttpRequestMethod(schemaversion.GetStringValue(fetchMap, "method")), + Url: schemaversion.GetStringValue(fetchMap, "url"), + } + + if body, ok := fetchMap["body"].(string); ok { + fetchOptions.Body = &body + } + + // Convert queryParams (2D array of strings) - preserve empty arrays + if queryParams, ok := fetchMap["queryParams"].([]interface{}); ok { + fetchOptions.QueryParams = convert2DStringArrayPreserveEmpty(queryParams) + } + + // Convert headers (2D array of strings) - preserve empty arrays + if headers, ok := fetchMap["headers"].([]interface{}); ok { + fetchOptions.Headers = convert2DStringArrayPreserveEmpty(headers) + } + + return fetchOptions +} + +func convertInfinityOptionsToV2(infinityMap map[string]interface{}) *dashv2alpha1.DashboardInfinityOptions { + infinityOptions := &dashv2alpha1.DashboardInfinityOptions{ + Method: dashv2alpha1.DashboardHttpRequestMethod(schemaversion.GetStringValue(infinityMap, "method")), + Url: schemaversion.GetStringValue(infinityMap, "url"), + DatasourceUid: schemaversion.GetStringValue(infinityMap, "datasourceUid"), + } + + if body, ok := infinityMap["body"].(string); ok { + infinityOptions.Body = &body + } + + if queryParams, ok := infinityMap["queryParams"].([]interface{}); ok { + infinityOptions.QueryParams = convert2DStringArrayPreserveEmpty(queryParams) + } + + if headers, ok := infinityMap["headers"].([]interface{}); ok { + infinityOptions.Headers = convert2DStringArrayPreserveEmpty(headers) + } + + return infinityOptions +} + +func convertActionVariablesToV2(variablesArray []interface{}) []dashv2alpha1.DashboardActionVariable { + if len(variablesArray) == 0 { + return nil + } + + result := make([]dashv2alpha1.DashboardActionVariable, 0, len(variablesArray)) + for _, variable := range variablesArray { + variableMap, ok := variable.(map[string]interface{}) + if !ok { + continue + } + + result = append(result, dashv2alpha1.DashboardActionVariable{ + Key: schemaversion.GetStringValue(variableMap, "key"), + Name: schemaversion.GetStringValue(variableMap, "name"), + Type: schemaversion.GetStringValue(variableMap, "type"), + }) + } + + return result +} + +func convertActionStyleToV2(styleMap map[string]interface{}) *dashv2alpha1.DashboardV2alpha1ActionStyle { + style := &dashv2alpha1.DashboardV2alpha1ActionStyle{} + + if backgroundColor, ok := styleMap["backgroundColor"].(string); ok { + style.BackgroundColor = &backgroundColor + } + + return style +} + +// convert2DStringArrayPreserveEmpty is like convert2DStringArray but returns +// an empty slice (not nil) when input is empty, to ensure JSON marshals as [] +func convert2DStringArrayPreserveEmpty(arr []interface{}) [][]string { + // Return empty slice (not nil) to preserve [] in JSON output + result := make([][]string, 0, len(arr)) + for _, item := range arr { + if innerArr, ok := item.([]interface{}); ok { + stringArr := make([]string, 0, len(innerArr)) + for _, s := range innerArr { + if str, ok := s.(string); ok { + stringArr = append(stringArr, str) + } + } + result = append(result, stringArr) + } + } + + return result +} + // getAngularPanelMigration is a convenience wrapper around schemaversion.GetAngularPanelMigration. // It checks if a panel type is an Angular panel and returns the new type to migrate to. // Returns the new panel type if migration is needed, empty string otherwise. diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go index 5dc7ecf21fd..18d9ae90814 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go @@ -1090,6 +1090,17 @@ func convertPanelKindToV1(panelKind *dashv2alpha1.DashboardPanelKind, panel map[ "id": t.Spec.Id, "options": t.Spec.Options, } + // Add disabled if set + if t.Spec.Disabled != nil { + transformation["disabled"] = *t.Spec.Disabled + } + // Add filter if set + if t.Spec.Filter != nil { + transformation["filter"] = map[string]interface{}{ + "id": t.Spec.Filter.Id, + "options": t.Spec.Filter.Options, + } + } transformations = append(transformations, transformation) } panel["transformations"] = transformations @@ -1985,9 +1996,18 @@ func convertFieldConfigDefaultsToV1(defaults *dashv2alpha1.DashboardFieldConfig) if defaults.Writeable != nil { result["writeable"] = *defaults.Writeable } + if defaults.FieldMinMax != nil { + result["fieldMinMax"] = *defaults.FieldMinMax + } + if defaults.NullValueMode != nil { + result["nullValueMode"] = string(*defaults.NullValueMode) + } if defaults.Links != nil { result["links"] = defaults.Links } + if len(defaults.Actions) > 0 { + result["actions"] = convertActionsToV1(defaults.Actions) + } if defaults.Color != nil { result["color"] = convertFieldColorToV1(defaults.Color) } @@ -2193,3 +2213,115 @@ func convertThresholdsToV1(thresholds *dashv2alpha1.DashboardThresholdsConfig) m return thresholdsMap } + +func convertActionsToV1(actions []dashv2alpha1.DashboardAction) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(actions)) + + for _, action := range actions { + actionMap := map[string]interface{}{ + "type": string(action.Type), + "title": action.Title, + } + + if action.Confirmation != nil { + actionMap["confirmation"] = *action.Confirmation + } + + if action.OneClick != nil { + actionMap["oneClick"] = *action.OneClick + } + + if action.Fetch != nil { + actionMap["fetch"] = convertFetchOptionsToV1(action.Fetch) + } + + if action.Infinity != nil { + actionMap["infinity"] = convertInfinityOptionsToV1(action.Infinity) + } + + if len(action.Variables) > 0 { + actionMap["variables"] = convertActionVariablesToV1(action.Variables) + } + + if action.Style != nil { + styleMap := map[string]interface{}{} + if action.Style.BackgroundColor != nil { + styleMap["backgroundColor"] = *action.Style.BackgroundColor + } + if len(styleMap) > 0 { + actionMap["style"] = styleMap + } + } + + result = append(result, actionMap) + } + + return result +} + +func convertFetchOptionsToV1(fetch *dashv2alpha1.DashboardFetchOptions) map[string]interface{} { + result := map[string]interface{}{ + "method": string(fetch.Method), + "url": fetch.Url, + } + + if fetch.Body != nil { + result["body"] = *fetch.Body + } + + if len(fetch.QueryParams) > 0 { + result["queryParams"] = convert2DStringArrayToInterface(fetch.QueryParams) + } + + if len(fetch.Headers) > 0 { + result["headers"] = convert2DStringArrayToInterface(fetch.Headers) + } + + return result +} + +func convertInfinityOptionsToV1(infinity *dashv2alpha1.DashboardInfinityOptions) map[string]interface{} { + result := map[string]interface{}{ + "method": string(infinity.Method), + "url": infinity.Url, + "datasourceUid": infinity.DatasourceUid, + } + + if infinity.Body != nil { + result["body"] = *infinity.Body + } + + if len(infinity.QueryParams) > 0 { + result["queryParams"] = convert2DStringArrayToInterface(infinity.QueryParams) + } + + if len(infinity.Headers) > 0 { + result["headers"] = convert2DStringArrayToInterface(infinity.Headers) + } + + return result +} + +func convertActionVariablesToV1(variables []dashv2alpha1.DashboardActionVariable) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(variables)) + for _, v := range variables { + result = append(result, map[string]interface{}{ + "key": v.Key, + "name": v.Name, + "type": v.Type, + }) + } + return result +} + +func convert2DStringArrayToInterface(arr [][]string) []interface{} { + result := make([]interface{}, 0, len(arr)) + for _, innerArr := range arr { + interfaceArr := make([]interface{}, 0, len(innerArr)) + for _, s := range innerArr { + interfaceArr = append(interfaceArr, s) + } + result = append(result, interfaceArr) + } + return result +} diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go index 45803b6d7ec..8435c56d83f 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go @@ -310,6 +310,9 @@ func convertFieldConfig_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardFieldConfi Links: in.Links, NoValue: in.NoValue, Custom: in.Custom, + FieldMinMax: in.FieldMinMax, + NullValueMode: (*dashv2beta1.DashboardNullValueMode)(in.NullValueMode), + Actions: convertActions_V2alpha1_to_V2beta1(in.Actions), } // Convert thresholds @@ -1021,3 +1024,59 @@ func convertAnnotationMappings_V2alpha1_to_V2beta1(in map[string]dashv2alpha1.Da } return out } + +func convertActions_V2alpha1_to_V2beta1(in []dashv2alpha1.DashboardAction) []dashv2beta1.DashboardAction { + if len(in) == 0 { + return nil + } + + out := make([]dashv2beta1.DashboardAction, len(in)) + for i, action := range in { + out[i] = dashv2beta1.DashboardAction{ + Type: dashv2beta1.DashboardActionType(action.Type), + Title: action.Title, + Confirmation: action.Confirmation, + OneClick: action.OneClick, + } + + if action.Fetch != nil { + out[i].Fetch = &dashv2beta1.DashboardFetchOptions{ + Method: dashv2beta1.DashboardHttpRequestMethod(action.Fetch.Method), + Url: action.Fetch.Url, + Body: action.Fetch.Body, + QueryParams: action.Fetch.QueryParams, + Headers: action.Fetch.Headers, + } + } + + if action.Infinity != nil { + out[i].Infinity = &dashv2beta1.DashboardInfinityOptions{ + Method: dashv2beta1.DashboardHttpRequestMethod(action.Infinity.Method), + Url: action.Infinity.Url, + Body: action.Infinity.Body, + QueryParams: action.Infinity.QueryParams, + Headers: action.Infinity.Headers, + DatasourceUid: action.Infinity.DatasourceUid, + } + } + + if len(action.Variables) > 0 { + out[i].Variables = make([]dashv2beta1.DashboardActionVariable, len(action.Variables)) + for j, v := range action.Variables { + out[i].Variables[j] = dashv2beta1.DashboardActionVariable{ + Key: v.Key, + Name: v.Name, + Type: v.Type, + } + } + } + + if action.Style != nil { + out[i].Style = &dashv2beta1.DashboardV2beta1ActionStyle{ + BackgroundColor: action.Style.BackgroundColor, + } + } + } + + return out +} diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-loki/loki_query_splitting.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-loki/loki_query_splitting.v42.json index a7beffa4cdc..8af239195cb 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-loki/loki_query_splitting.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/datasource-loki/loki_query_splitting.v42.json @@ -219,8 +219,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -312,8 +311,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -492,8 +490,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -584,8 +581,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -676,8 +672,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -791,8 +786,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -906,8 +900,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -1022,8 +1015,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests.v42.json index 635103053bf..87b63411976 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests.v42.json @@ -65,17 +65,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -136,17 +133,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -207,17 +201,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -271,7 +262,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [], "max": 100, "min": 0, @@ -279,17 +269,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -342,7 +329,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [], "max": 100, "min": 0, @@ -350,17 +336,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -414,7 +397,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [], "max": 100, "min": 0, @@ -422,17 +404,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -485,7 +464,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [], "max": 100, "min": 0, @@ -493,17 +471,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -682,7 +657,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [ { "options": { @@ -699,17 +673,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -764,7 +735,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [ { "options": { @@ -782,17 +752,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -847,7 +814,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [ { "options": { @@ -866,17 +832,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -931,7 +894,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [ { "options": { @@ -960,17 +922,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -1052,7 +1011,7 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "2", + "decimals": 2, "mappings": [], "max": 100, "min": 0, @@ -1060,17 +1019,14 @@ "mode": "absolute", "steps": [ { - "color": "#7EB26D", - "index": 0 + "color": "#7EB26D" }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-histogram/histogram_tests.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-histogram/histogram_tests.v42.json index 7e392bd55d0..6c521eaec9b 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-histogram/histogram_tests.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-histogram/histogram_tests.v42.json @@ -58,8 +58,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -127,8 +126,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -196,8 +194,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -277,8 +274,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -355,8 +351,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -448,8 +443,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -536,8 +530,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [ { @@ -619,8 +612,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [ { @@ -702,8 +694,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [ { @@ -785,8 +776,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -850,8 +840,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [ { diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-xychart/xychart-tooltip-color-test.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-xychart/xychart-tooltip-color-test.v42.json index 417ea1661e1..f28fee864e5 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-xychart/xychart-tooltip-color-test.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-xychart/xychart-tooltip-color-test.v42.json @@ -61,8 +61,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -148,8 +147,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -235,8 +233,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -322,8 +319,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -416,8 +412,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -510,8 +505,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -604,8 +598,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, diff --git a/devenv/dev-dashboards/datasource-loki/loki_query_splitting.json b/devenv/dev-dashboards/datasource-loki/loki_query_splitting.json index 0cc5135a0f4..44577d49ac4 100644 --- a/devenv/dev-dashboards/datasource-loki/loki_query_splitting.json +++ b/devenv/dev-dashboards/datasource-loki/loki_query_splitting.json @@ -216,8 +216,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -311,8 +310,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -493,8 +491,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -584,8 +581,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -675,8 +671,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -789,8 +784,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -903,8 +897,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -1018,8 +1011,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests.json b/devenv/dev-dashboards/panel-gauge/gauge_tests.json index f32ace420b4..f76f5d8809a 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests.json @@ -51,17 +51,14 @@ "steps": [ { "color": "#7EB26D", - "index": 0, "value": null }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -122,17 +119,14 @@ "steps": [ { "color": "#7EB26D", - "index": 0, "value": null }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -193,17 +187,14 @@ "steps": [ { "color": "#7EB26D", - "index": 0, "value": null }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -255,7 +246,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [], "max": 100, "min": 0, @@ -264,17 +254,14 @@ "steps": [ { "color": "#7EB26D", - "index": 0, "value": null }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -326,7 +313,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [], "max": 100, "min": 0, @@ -335,17 +321,14 @@ "steps": [ { "color": "#7EB26D", - "index": 0, "value": null }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -397,7 +380,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [], "max": 100, "min": 0, @@ -406,17 +388,14 @@ "steps": [ { "color": "#7EB26D", - "index": 0, "value": null }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -468,7 +447,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [], "max": 100, "min": 0, @@ -477,17 +455,14 @@ "steps": [ { "color": "#7EB26D", - "index": 0, "value": null }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -641,7 +616,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [ { "from": "", @@ -660,17 +634,14 @@ "steps": [ { "color": "#7EB26D", - "index": 0, "value": null }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -723,7 +694,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [ { "from": "", @@ -742,17 +712,14 @@ "steps": [ { "color": "#7EB26D", - "index": 0, "value": null }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -805,7 +772,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [ { "from": "0", @@ -824,17 +790,14 @@ "steps": [ { "color": "#7EB26D", - "index": 0, "value": null }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -887,7 +850,6 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "", "mappings": [ { "from": "0", @@ -915,17 +877,14 @@ "steps": [ { "color": "#7EB26D", - "index": 0, "value": null }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -991,7 +950,7 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "2", + "decimals": 2, "mappings": [], "max": 100, "min": 0, @@ -1000,17 +959,14 @@ "steps": [ { "color": "#7EB26D", - "index": 0, "value": null }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -1071,7 +1027,7 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "2", + "decimals": 2, "mappings": [], "max": 100, "min": 0, @@ -1080,17 +1036,14 @@ "steps": [ { "color": "#7EB26D", - "index": 0, "value": null }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -1152,7 +1105,7 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "2", + "decimals": 2, "mappings": [], "max": 100, "min": 0, @@ -1161,17 +1114,14 @@ "steps": [ { "color": "#7EB26D", - "index": 0, "value": null }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] @@ -1233,7 +1183,7 @@ "mode": "thresholds" }, "custom": {}, - "decimals": "2", + "decimals": 2, "mappings": [], "max": 100, "min": 0, @@ -1242,17 +1192,14 @@ "steps": [ { "color": "#7EB26D", - "index": 0, "value": null }, { "color": "#ef843c", - "index": 1, "value": 75 }, { "color": "#e24d42", - "index": 2, "value": 90 } ] diff --git a/devenv/dev-dashboards/panel-histogram/histogram_tests.json b/devenv/dev-dashboards/panel-histogram/histogram_tests.json index af6127cb447..7d6a684417e 100644 --- a/devenv/dev-dashboards/panel-histogram/histogram_tests.json +++ b/devenv/dev-dashboards/panel-histogram/histogram_tests.json @@ -58,8 +58,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -125,8 +124,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -192,8 +190,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -269,8 +266,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -343,8 +339,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -432,8 +427,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -516,8 +510,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [ { @@ -597,8 +590,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [ { @@ -678,8 +670,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [ { @@ -759,8 +750,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -824,8 +814,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [ { diff --git a/devenv/dev-dashboards/panel-xychart/xychart-tooltip-color-test.json b/devenv/dev-dashboards/panel-xychart/xychart-tooltip-color-test.json index b0ae2a9d76b..15fda5d6316 100644 --- a/devenv/dev-dashboards/panel-xychart/xychart-tooltip-color-test.json +++ b/devenv/dev-dashboards/panel-xychart/xychart-tooltip-color-test.json @@ -62,8 +62,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -150,8 +149,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -238,8 +236,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -326,8 +323,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -421,8 +417,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -516,8 +511,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, @@ -611,8 +605,7 @@ "value": 80 } ] - }, - "unitScale": true + } }, "overrides": [] }, diff --git a/go.work.sum b/go.work.sum index 5f97bbdd6ef..d064248a16a 100644 --- a/go.work.sum +++ b/go.work.sum @@ -259,6 +259,7 @@ codeberg.org/go-latex/latex v0.1.0 h1:hoGO86rIbWVyjtlDLzCqZPjNykpWQ9YuTZqAzPcfL3 codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw= codeberg.org/go-pdf/fpdf v0.10.0 h1:u+w669foDDx5Ds43mpiiayp40Ov6sZalgcPMDBcZRd4= codeberg.org/go-pdf/fpdf v0.10.0/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= contrib.go.opencensus.io/exporter/ocagent v0.6.0 h1:Z1n6UAyr0QwM284yUuh5Zd8JlvxUGAhFZcgMJkMPrGM= contrib.go.opencensus.io/exporter/prometheus v0.4.0/go.mod h1:o7cosnyfuPVK0tB8q0QmaQNhGnptITnPQB+z1+qeFB0= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= @@ -1000,6 +1001,7 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36 github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f h1:9tRhudagkQO2s61SLFLSziIdCm7XlkfypVKDxpcHokg= github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f/go.mod h1:AsVdCBeDFN9QbgpJg+8voDAcgsW0RmNvBd70ecMMdC0= github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae/go.mod h1:6CJ1uXmLZ13ufpO9xE4pST+DyaBt0uszzrV0YnoaVLQ= github.com/grafana/sqlds/v4 v4.2.4/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU= github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU= github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0 h1:bjh0PVYSVVFxzINqPFYJmAmJNrWPgnVjuSdYJGHmtFU= diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json index b89d431883b..244a8e591b1 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -1788,11 +1788,11 @@ "default": false }, "valuesFormat": { + "type": "string", "enum": [ "csv", "json" - ], - "type": "string" + ] } }, "additionalProperties": false @@ -2242,6 +2242,10 @@ "description": "This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.", "type": "string" }, + "fieldMinMax": { + "description": "Calculate min max per field", + "type": "boolean" + }, "filterable": { "description": "True if data source field supports ad-hoc filters", "type": "boolean" @@ -2273,6 +2277,9 @@ "description": "Alternative to empty string", "type": "string" }, + "nullValueMode": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardNullValueMode" + }, "path": { "description": "An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results", "type": "string" @@ -2281,7 +2288,7 @@ "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThresholdsConfig" }, "unit": { - "description": "Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n`suffix:` for custom unit that should go after value.\n`prefix:` for custom unit that should go before value.\n`time:` For custom date time formats type for example `time:YYYY-MM-DD`.\n`si:` 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.\n`count:` for a custom count unit.\n`currency:` for custom a currency unit.", + "description": "Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n`suffix:\u003csuffix\u003e` for custom unit that should go after value.\n`prefix:\u003cprefix\u003e` for custom unit that should go before value.\n`time:\u003cformat\u003e` For custom date time formats type for example `time:YYYY-MM-DD`.\n`si:\u003cbase scale\u003e\u003cunit characters\u003e` 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.\n`count:\u003cunit\u003e` for a custom count unit.\n`currency:\u003cunit\u003e` for custom a currency unit.", "type": "string" }, "writeable": { @@ -2774,6 +2781,15 @@ }, "additionalProperties": false }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardNullValueMode": { + "description": "How null values should be handled", + "type": "string", + "enum": [ + "null", + "connected", + "null as zero" + ] + }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelKind": { "type": "object", "required": [ @@ -3797,7 +3813,7 @@ ], "properties": { "options": { - "description": "Map with : ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }", + "description": "Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }", "type": "object", "additionalProperties": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult" @@ -4221,7 +4237,7 @@ } }, "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { - "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:', where is the name of a field in a struct, or key in a map 'v:', where is the exact json formatted value of a list item 'i:', where is position of a item in a list 'k:', where is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", "type": "object" }, "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta": { @@ -4575,4 +4591,4 @@ } } } -} +} \ No newline at end of file diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json index 7f396bc20d4..8588ee9707a 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json @@ -2261,6 +2261,10 @@ "description": "This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.", "type": "string" }, + "fieldMinMax": { + "description": "Calculate min max per field", + "type": "boolean" + }, "filterable": { "description": "True if data source field supports ad-hoc filters", "type": "boolean" @@ -2292,6 +2296,9 @@ "description": "Alternative to empty string", "type": "string" }, + "nullValueMode": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardNullValueMode" + }, "path": { "description": "An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results", "type": "string" @@ -2300,7 +2307,7 @@ "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardThresholdsConfig" }, "unit": { - "description": "Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n`suffix:` for custom unit that should go after value.\n`prefix:` for custom unit that should go before value.\n`time:` For custom date time formats type for example `time:YYYY-MM-DD`.\n`si:` 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.\n`count:` for a custom count unit.\n`currency:` for custom a currency unit.", + "description": "Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n`suffix:\u003csuffix\u003e` for custom unit that should go after value.\n`prefix:\u003cprefix\u003e` for custom unit that should go before value.\n`time:\u003cformat\u003e` For custom date time formats type for example `time:YYYY-MM-DD`.\n`si:\u003cbase scale\u003e\u003cunit characters\u003e` 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.\n`count:\u003cunit\u003e` for a custom count unit.\n`currency:\u003cunit\u003e` for custom a currency unit.", "type": "string" }, "writeable": { @@ -2803,6 +2810,15 @@ }, "additionalProperties": false }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardNullValueMode": { + "description": "How null values should be handled", + "type": "string", + "enum": [ + "null", + "connected", + "null as zero" + ] + }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardPanelKind": { "type": "object", "required": [ @@ -3823,7 +3839,7 @@ ], "properties": { "options": { - "description": "Map with : ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }", + "description": "Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }", "type": "object", "additionalProperties": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMappingResult" @@ -4252,7 +4268,7 @@ } }, "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { - "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:', where is the name of a field in a struct, or key in a map 'v:', where is the exact json formatted value of a list item 'i:', where is position of a item in a list 'k:', where is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", "type": "object" }, "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta": { @@ -4606,4 +4622,4 @@ } } } -} +} \ No newline at end of file diff --git a/public/app/features/dashboard-scene/serialization/serialization-test-utils.ts b/public/app/features/dashboard-scene/serialization/serialization-test-utils.ts index 178c89e1da4..af7d6021fb5 100644 --- a/public/app/features/dashboard-scene/serialization/serialization-test-utils.ts +++ b/public/app/features/dashboard-scene/serialization/serialization-test-utils.ts @@ -1,9 +1,40 @@ +import { readdirSync, statSync } from 'fs'; +import path from 'path'; + import { Spec as DashboardV2Spec, GridLayoutItemKind, RowsLayoutRowKind, } from '@grafana/schema/dist/esm/schema/dashboard/v2'; +/** + * Recursively gets all JSON files from a directory. + * Returns an array of objects containing the full file path and relative path from the base directory. + */ +export function getFilesRecursively( + dir: string, + baseDir: string = dir +): Array<{ filePath: string; relativePath: string }> { + const files: Array<{ filePath: string; relativePath: string }> = []; + const entries = readdirSync(dir); + + for (const entry of entries) { + const fullPath = path.join(dir, entry); + const stat = statSync(fullPath); + + if (stat.isDirectory()) { + files.push(...getFilesRecursively(fullPath, baseDir)); + } else if (entry.endsWith('.json')) { + files.push({ + filePath: fullPath, + relativePath: path.relative(baseDir, fullPath), + }); + } + } + + return files; +} + /** * Normalizes backend output to match frontend behavior. * The backend sets repeat properties on library panel grid items from the library panel definition, @@ -94,3 +125,40 @@ export function normalizeBackendOutputForFrontendComparison( return normalized; } + +/** + * Recursively removes empty arrays from an object. + * This normalizes the difference between frontend (which preserves empty arrays) + * and Go backend (which omits empty arrays due to `omitempty`). + */ +export function removeEmptyArrays(value: T): T { + if (Array.isArray(value)) { + // Recursively process array items, but don't remove the array itself here + // (parent will handle removal if this array becomes empty after processing) + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return value.map((item) => removeEmptyArrays(item)) as T; + } + + if (value !== null && typeof value === 'object') { + const result: Record = {}; + for (const key of Object.keys(value)) { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const v = (value as Record)[key]; + if (Array.isArray(v)) { + // Only include non-empty arrays + if (v.length > 0) { + result[key] = removeEmptyArrays(v); + } + // Skip empty arrays (don't add to result) + } else if (v !== null && typeof v === 'object') { + result[key] = removeEmptyArrays(v); + } else { + result[key] = v; + } + } + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return result as T; + } + + return value; +} diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelV1ToV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelV1ToV2.test.ts index a870f4feef5..afd8dd97c03 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelV1ToV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelV1ToV2.test.ts @@ -1,9 +1,13 @@ -import { readdirSync, readFileSync } from 'fs'; +import { existsSync, readFileSync } from 'fs'; import path from 'path'; import { getSceneCreationOptions } from '../pages/DashboardScenePageStateManager'; -import { normalizeBackendOutputForFrontendComparison } from './serialization-test-utils'; +import { + getFilesRecursively, + normalizeBackendOutputForFrontendComparison, + removeEmptyArrays, +} from './serialization-test-utils'; import { transformSaveModelSchemaV2ToScene } from './transformSaveModelSchemaV2ToScene'; import { transformSaveModelToScene } from './transformSaveModelToScene'; import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSchemaV2'; @@ -173,19 +177,32 @@ describe('V1 to V2 Dashboard Transformation Comparison', () => { 'migrated_dashboards_output' ); - const jsonInputs = readdirSync(inputDir); const LATEST_API_VERSION = 'dashboard.grafana.app/v2beta1'; - // Filter to only process v1beta1 input files - const v1beta1Inputs = jsonInputs.filter((inputFile) => inputFile.startsWith('v1beta1.')); + // Get v0alpha1 and v1beta1 input files recursively from all subdirectories + const v1beta1Inputs = getFilesRecursively(inputDir).filter(({ relativePath }) => { + const fileName = path.basename(relativePath); + return fileName.startsWith('v1beta1.') && fileName.endsWith('.json'); + }); - v1beta1Inputs.forEach((inputFile) => { - it(`compare ${inputFile} from v1beta1 to v2beta1 backend and frontend conversions`, async () => { - const jsonInput = JSON.parse(readFileSync(path.join(inputDir, inputFile), 'utf8')); + v1beta1Inputs.forEach(({ filePath: inputFilePath, relativePath }) => { + // Calculate output file path for this input + const relativeDir = path.dirname(relativePath); + const fileName = path.basename(relativePath); + const outputFileName = fileName.replace('.json', `.${LATEST_API_VERSION.split('/')[1]}.json`); + const outputFilePath = + relativeDir === '.' ? path.join(outputDir, outputFileName) : path.join(outputDir, relativeDir, outputFileName); - // Find the corresponding v2beta1 output file - const outputFileName = inputFile.replace('.json', `.${LATEST_API_VERSION.split('/')[1]}.json`); - const outputFilePath = path.join(outputDir, outputFileName); + // Include output file name in test description for clarity + const outputRelativePath = relativeDir === '.' ? outputFileName : path.join(relativeDir, outputFileName); + + it(`compare ${relativePath} → ${outputRelativePath}`, async () => { + // Skip if output file doesn't exist + if (!existsSync(outputFilePath)) { + return; + } + + const jsonInput = JSON.parse(readFileSync(inputFilePath, 'utf8')); // Load the backend output const backendOutput = JSON.parse(readFileSync(outputFilePath, 'utf8')); @@ -202,10 +219,11 @@ describe('V1 to V2 Dashboard Transformation Comparison', () => { }); const backendOutputAfterLoadedByScene = transformSceneToSaveModelSchemaV2(sceneBackend, false); - // Transform using frontend path: v1beta1 -> Scene -> v2beta1 - // Extract the spec from v1beta1 format and use it as the dashboard data - // Remove snapshot field to prevent isSnapshot() from returning true - const dashboardSpec = { ...jsonInput.spec }; + // Determine how to extract the dashboard spec: + // - Files with apiVersion field are API-wrapped (spec contains dashboard) + // - Files without apiVersion are raw dashboard JSON (entire file is the spec) + const hasApiVersion = jsonInput.apiVersion !== undefined; + const dashboardSpec = hasApiVersion ? { ...jsonInput.spec } : { ...jsonInput }; delete dashboardSpec.snapshot; // Wrap in DashboardDTO structure that transformSaveModelToScene expects @@ -238,30 +256,39 @@ describe('V1 to V2 Dashboard Transformation Comparison', () => { // Normalize backend output to account for differences in library panel repeat handling // Backend sets repeat from library panel definition, frontend only sets it when explicit on instance - // For migrated dashboards, panels are in the root level, not in spec.panels - const inputPanels = jsonInput.panels || jsonInput.spec?.panels || []; - const normalizedBackendOutput = normalizeBackendOutputForFrontendComparison( - backendOutputAfterLoadedByScene, - inputPanels + // Get input panels from appropriate location based on file format + const inputPanels = hasApiVersion ? jsonInput.spec?.panels || [] : jsonInput.panels || []; + const normalizedBackendOutput = removeEmptyArrays( + normalizeBackendOutputForFrontendComparison(backendOutputAfterLoadedByScene, inputPanels) ); + // Also normalize frontend output to remove schema gap fields and empty arrays + // (Go backend omits empty arrays due to omitempty, frontend preserves them) + const normalizedFrontendOutput = removeEmptyArrays(frontendOutput); + // Compare only the spec structures - this is the core transformation - expect(normalizedBackendOutput).toEqual(frontendOutput); + expect(normalizedBackendOutput).toEqual(normalizedFrontendOutput); }); }); // Test migrated dashboards (from migration pipeline output) - const migratedJsonInputs = readdirSync(migratedInput); + const migratedJsonInputs = getFilesRecursively(migratedInput).filter(({ relativePath }) => { + return relativePath.endsWith('.json'); + }); - migratedJsonInputs.forEach((inputFile) => { - it(`compare migrated ${inputFile} from v1beta1 to v2beta1 backend and frontend conversions`, async () => { + migratedJsonInputs.forEach(({ filePath: inputFilePath, relativePath }) => { + // Calculate output file path for this input + const relativeDir = path.dirname(relativePath); + const fileName = path.basename(relativePath); + const outputFileName = `v1beta1-mig-${fileName.replace('.json', '')}.${LATEST_API_VERSION.split('/')[1]}.json`; + const outputFilePath = + relativeDir === '.' + ? path.join(migratedOutput, outputFileName) + : path.join(migratedOutput, relativeDir, outputFileName); + + it(`compare migrated ${relativePath} → ${outputFileName}`, async () => { // Read the raw dashboard JSON from migration output (latest_version directory) - const jsonInput = JSON.parse(readFileSync(path.join(migratedInput, inputFile), 'utf8')); - - // Find the corresponding v2beta1 output file in migrated_dashboards_output - // The backend test prefixes these with "v1beta1-mig-" - const outputFileName = `v1beta1-mig-${inputFile.replace('.json', '')}.${LATEST_API_VERSION.split('/')[1]}.json`; - const outputFilePath = path.join(migratedOutput, outputFileName); + const jsonInput = JSON.parse(readFileSync(inputFilePath, 'utf8')); // Load the backend output const backendOutput = JSON.parse(readFileSync(outputFilePath, 'utf8')); @@ -316,13 +343,16 @@ describe('V1 to V2 Dashboard Transformation Comparison', () => { // Backend sets repeat from library panel definition, frontend only sets it when explicit on instance // For migrated dashboards, panels are in the root level, not in spec.panels const inputPanels = jsonInput.panels || jsonInput.spec?.panels || []; - const normalizedBackendOutput = normalizeBackendOutputForFrontendComparison( - backendOutputAfterLoadedByScene, - inputPanels + const normalizedBackendOutput = removeEmptyArrays( + normalizeBackendOutputForFrontendComparison(backendOutputAfterLoadedByScene, inputPanels) ); + // Also normalize frontend output to remove schema gap fields and empty arrays + // (Go backend omits empty arrays due to omitempty, frontend preserves them) + const normalizedFrontendOutput = removeEmptyArrays(frontendOutput); + // Compare only the spec structures - this is the core transformation - expect(normalizedBackendOutput).toEqual(frontendOutput); + expect(normalizedBackendOutput).toEqual(normalizedFrontendOutput); }); }); }); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelV2ToV1.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelV2ToV1.test.ts index af6116b9adf..bca864088ed 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelV2ToV1.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelV2ToV1.test.ts @@ -1,4 +1,4 @@ -import { existsSync, readdirSync, readFileSync, statSync } from 'fs'; +import { existsSync, readFileSync } from 'fs'; import path from 'path'; import { Dashboard } from '@grafana/schema'; @@ -8,32 +8,11 @@ import { DashboardDataDTO } from 'app/types/dashboard'; import { getSceneCreationOptions } from '../pages/DashboardScenePageStateManager'; +import { getFilesRecursively } from './serialization-test-utils'; import { transformSaveModelSchemaV2ToScene } from './transformSaveModelSchemaV2ToScene'; import { transformSaveModelToScene } from './transformSaveModelToScene'; import { transformSceneToSaveModel } from './transformSceneToSaveModel'; -// Helper function to recursively get all files from a directory -function getFilesRecursively(dir: string, baseDir: string = dir): Array<{ filePath: string; relativePath: string }> { - const files: Array<{ filePath: string; relativePath: string }> = []; - const entries = readdirSync(dir); - - for (const entry of entries) { - const fullPath = path.join(dir, entry); - const stat = statSync(fullPath); - - if (stat.isDirectory()) { - files.push(...getFilesRecursively(fullPath, baseDir)); - } else if (entry.endsWith('.json')) { - files.push({ - filePath: fullPath, - relativePath: path.relative(baseDir, fullPath), - }); - } - } - - return files; -} - // Mock the config to provide datasource information jest.mock('@grafana/runtime', () => { const mockConfig = { diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index 3052e4d8118..c5d01b433e3 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -140,7 +140,7 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa const dashboard: Dashboard = { ...defaultDashboard, title: state.title, - description: state.description || undefined, + description: state.description, uid: state.uid, id: state.id, editable: state.editable, diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index 90c7f5e2e61..bd1ebbbe6bf 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -88,7 +88,7 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps const dashboardSchemaV2: DeepPartial = { //dashboard settings title: sceneDash.title, - description: sceneDash.description, + description: sceneDash.description || undefined, cursorSync: getCursorSync(sceneDash), liveNow: getLiveNow(sceneDash), preload: sceneDash.preload ?? defaultDashboardV2Spec().preload, From a01777eafa016203700d250bc84f7eaa3c82d3ad Mon Sep 17 00:00:00 2001 From: Jo Date: Tue, 13 Jan 2026 16:11:15 +0100 Subject: [PATCH 06/19] docs: improve RBAC and role creation documentation (#116188) * docs: improve RBAC and role creation documentation - Clarify that file-based RBAC provisioning is for self-managed instances only - Distinguish between Grafana Admin (Server Admin) and Org Admin - Remove incorrect UI instructions for custom role creation - Add Terraform example for creating custom roles and assignments * Apply suggestions from code review Co-authored-by: Anna Urbiztondo --------- Co-authored-by: Anna Urbiztondo --- .../roles-and-permissions/_index.md | 8 +- .../access-control/manage-rbac-roles/index.md | 161 +++++++++++------- .../rbac-grafana-provisioning/index.md | 5 +- 3 files changed, 109 insertions(+), 65 deletions(-) diff --git a/docs/sources/administration/roles-and-permissions/_index.md b/docs/sources/administration/roles-and-permissions/_index.md index c8135836fa1..7a33d940a15 100644 --- a/docs/sources/administration/roles-and-permissions/_index.md +++ b/docs/sources/administration/roles-and-permissions/_index.md @@ -35,10 +35,10 @@ For Grafana Cloud users, Grafana Support is not authorised to make org role chan ## Grafana server administrators -A Grafana server administrator manages server-wide settings and access to resources such as organizations, users, and licenses. Grafana includes a default server administrator that you can use to manage all of Grafana, or you can divide that responsibility among other server administrators that you create. +A Grafana server administrator (sometimes referred to as a **Grafana Admin**) manages server-wide settings and access to resources such as organizations, users, and licenses. Grafana includes a default server administrator that you can use to manage all of Grafana, or you can divide that responsibility among other server administrators that you create. -{{< admonition type="note" >}} -The server administrator role does not mean that the user is also a Grafana [organization administrator](#organization-roles). +{{< admonition type="caution" >}} +The server administrator role is distinct from the [organization administrator](#organization-roles) role. {{< /admonition >}} A server administrator can perform the following tasks: @@ -50,7 +50,7 @@ A server administrator can perform the following tasks: - Upgrade the server to Grafana Enterprise. {{< admonition type="note" >}} -The server administrator role does not exist in Grafana Cloud. +The server administrator (Grafana Admin) role does not exist in Grafana Cloud. {{< /admonition >}} To assign or remove server administrator privileges, see [Server user management](../user-management/server-user-management/assign-remove-server-admin-privileges/). diff --git a/docs/sources/administration/roles-and-permissions/access-control/manage-rbac-roles/index.md b/docs/sources/administration/roles-and-permissions/access-control/manage-rbac-roles/index.md index 40a6d3645af..b0f35087efc 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/manage-rbac-roles/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/manage-rbac-roles/index.md @@ -53,6 +53,11 @@ refs: destination: /docs/grafana//administration/roles-and-permissions/access-control/custom-role-actions-scopes/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/account-management/authentication-and-permissions/access-control/custom-role-actions-scopes/ + rbac-terraform-provisioning: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/roles-and-permissions/access-control/rbac-terraform-provisioning/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/account-management/authentication-and-permissions/access-control/rbac-terraform-provisioning/ rbac-grafana-provisioning: - pattern: /docs/grafana/ destination: /docs/grafana//administration/roles-and-permissions/access-control/rbac-grafana-provisioning/ @@ -145,7 +150,13 @@ Refer to the [RBAC HTTP API](ref:api-rbac-get-a-role) for more details. ## Create custom roles -This section shows you how to create a custom RBAC role using Grafana provisioning and the HTTP API. +This section shows you how to create a custom RBAC role using Grafana provisioning or the HTTP API. + +Creating and editing custom roles is not currently possible in the Grafana UI. To manage custom roles, use one of the following methods: + +- [Provisioning](ref:rbac-grafana-provisioning) (for self-managed instances) +- [HTTP API](ref:api-rbac-create-a-new-custom-role) +- [Terraform](ref:rbac-terraform-provisioning) Create a custom role when basic roles and fixed roles do not meet your permissions requirements. @@ -153,14 +164,101 @@ Create a custom role when basic roles and fixed roles do not meet your permissio - [Plan your RBAC rollout strategy](ref:plan-rbac-rollout-strategy). - Determine which permissions you want to add to the custom role. To see a list of actions and scope, refer to [RBAC permissions, actions, and scopes](ref:custom-role-actions-scopes). -- [Enable role provisioning](ref:rbac-grafana-provisioning). - Ensure that you have permissions to create a custom role. - By default, the Grafana Admin role has permission to create custom roles. - A Grafana Admin can delegate the custom role privilege to another user by creating a custom role with the relevant permissions and adding the `permissions:type:delegate` scope. -### Create custom roles using provisioning +### Create custom roles using the HTTP API -[File-based provisioning](ref:rbac-grafana-provisioning) is one method you can use to create custom roles. +The following examples show you how to create a custom role using the Grafana HTTP API. For more information about the HTTP API, refer to [Create a new custom role](ref:api-rbac-create-a-new-custom-role). + +{{< admonition type="note" >}} +When you create a custom role you can only give it the same permissions you already have. For example, if you only have `users:create` permissions, then you can't create a role that includes other permissions. +{{< /admonition >}} + +The following example creates a `custom:users:admin` role and assigns the `users:create` action to it. + +**Example request** + +``` +curl --location --request POST '/api/access-control/roles/' \ +--header 'Authorization: Basic YWRtaW46cGFzc3dvcmQ=' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "version": 1, + "uid": "jZrmlLCkGksdka", + "name": "custom:users:admin", + "displayName": "custom users admin", + "description": "My custom role which gives users permissions to create users", + "global": true, + "permissions": [ + { + "action": "users:create" + } + ] +}' +``` + +**Example response** + +``` +{ + "version": 1, + "uid": "jZrmlLCkGksdka", + "name": "custom:users:admin", + "displayName": "custom users admin", + "description": "My custom role which gives users permissions to create users", + "global": true, + "permissions": [ + { + "action": "users:create" + "updated": "2021-05-17T22:07:31.569936+02:00", + "created": "2021-05-17T22:07:31.569935+02:00" + } + ], + "updated": "2021-05-17T22:07:31.564403+02:00", + "created": "2021-05-17T22:07:31.564403+02:00" +} +``` + +Refer to the [RBAC HTTP API](ref:api-rbac-create-a-new-custom-role) for more details. + +### Create custom roles using Terraform + +You can use the [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs) to manage custom roles and their assignments. This is the recommended method for Grafana Cloud users who want to manage RBAC as code. For more information, refer to [Provisioning RBAC with Terraform](ref:rbac-terraform-provisioning). + +The following example creates a custom role and assigns it to a team: + +```terraform +resource "grafana_role" "custom_folder_manager" { + name = "custom:folders:manager" + description = "Custom role for reading and creating folders" + uid = "custom-folders-manager" + version = 1 + global = true + + permissions { + action = "folders:read" + scope = "folders:*" + } + + permissions { + action = "folders:create" + scope = "folders:uid:general" # Allows creating folders at the root level + } +} + +resource "grafana_role_assignment" "custom_folder_manager_assignment" { + role_uid = grafana_role.custom_folder_manager.uid + teams = [""] +} +``` + +For more information, refer to the [`grafana_role`](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/role) and [`grafana_role_assignment`](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/role_assignment) documentation in the Terraform Registry. + +### Create custom roles using file-based provisioning + +You can use [file-based provisioning](ref:rbac-grafana-provisioning) to create custom roles for self-managed instances. 1. Open the YAML configuration file and locate the `roles` section. @@ -251,61 +349,6 @@ roles: state: 'absent' ``` -### Create custom roles using the HTTP API - -The following examples show you how to create a custom role using the Grafana HTTP API. For more information about the HTTP API, refer to [Create a new custom role](ref:api-rbac-create-a-new-custom-role). - -{{< admonition type="note" >}} -You cannot create a custom role with permissions that you do not have. For example, if you only have `users:create` permissions, then you cannot create a role that includes other permissions. -{{< /admonition >}} - -The following example creates a `custom:users:admin` role and assigns the `users:create` action to it. - -**Example request** - -``` -curl --location --request POST '/api/access-control/roles/' \ ---header 'Authorization: Basic YWRtaW46cGFzc3dvcmQ=' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "version": 1, - "uid": "jZrmlLCkGksdka", - "name": "custom:users:admin", - "displayName": "custom users admin", - "description": "My custom role which gives users permissions to create users", - "global": true, - "permissions": [ - { - "action": "users:create" - } - ] -}' -``` - -**Example response** - -``` -{ - "version": 1, - "uid": "jZrmlLCkGksdka", - "name": "custom:users:admin", - "displayName": "custom users admin", - "description": "My custom role which gives users permissions to create users", - "global": true, - "permissions": [ - { - "action": "users:create" - "updated": "2021-05-17T22:07:31.569936+02:00", - "created": "2021-05-17T22:07:31.569935+02:00" - } - ], - "updated": "2021-05-17T22:07:31.564403+02:00", - "created": "2021-05-17T22:07:31.564403+02:00" -} -``` - -Refer to the [RBAC HTTP API](ref:api-rbac-create-a-new-custom-role) for more details. - ## Update basic role permissions If the default basic role definitions do not meet your requirements, you can change their permissions. diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-grafana-provisioning/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-grafana-provisioning/index.md index 06f9699533b..fe45a620bc5 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-grafana-provisioning/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-grafana-provisioning/index.md @@ -6,7 +6,6 @@ description: Learn about RBAC Grafana provisioning and view an example YAML prov file that configures Grafana role assignments. labels: products: - - cloud - enterprise menuTitle: Provisioning RBAC with Grafana title: Provisioning RBAC with Grafana @@ -52,11 +51,13 @@ refs: # Provisioning RBAC with Grafana {{< admonition type="note" >}} -Available in [Grafana Enterprise](/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud). +Available in [Grafana Enterprise](/docs/grafana//introduction/grafana-enterprise/) for self-managed instances. This feature is not available in Grafana Cloud. {{< /admonition >}} You can create, change or remove [Custom roles](ref:manage-rbac-roles-create-custom-roles-using-provisioning) and create or remove [basic role assignments](ref:assign-rbac-roles-assign-a-fixed-role-to-a-basic-role-using-provisioning), by adding one or more YAML configuration files in the `provisioning/access-control/` directory. +Because this method requires access to the file system where Grafana is running, it's only available for self-managed Grafana instances. To provision RBAC in Grafana Cloud, use [Terraform](ref:rbac-terraform-provisioning) or the [HTTP API](ref:api-rbac-create-and-manage-custom-roles). + Grafana performs provisioning during startup. After you make a change to the configuration file, you can reload it during runtime. You do not need to restart the Grafana server for your changes to take effect. **Before you begin:** From fe5aa3e2810547ce31b09ff45d0f39f46d606ae8 Mon Sep 17 00:00:00 2001 From: Yunwen Zheng Date: Tue, 13 Jan 2026 10:20:17 -0500 Subject: [PATCH 07/19] RecentlyViewedDashboards: UI tweaks (#116171) --- .../components/RecentlyViewedDashboards.tsx | 1 + .../plugins/panel/dashlist/DashListItem.tsx | 51 +++++++++++-------- public/app/plugins/panel/dashlist/styles.ts | 4 ++ 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/public/app/features/browse-dashboards/components/RecentlyViewedDashboards.tsx b/public/app/features/browse-dashboards/components/RecentlyViewedDashboards.tsx index f0c2d4d3a13..b11e7c35f85 100644 --- a/public/app/features/browse-dashboards/components/RecentlyViewedDashboards.tsx +++ b/public/app/features/browse-dashboards/components/RecentlyViewedDashboards.tsx @@ -123,6 +123,7 @@ const getStyles = (theme: GrafanaTheme2) => { color: 'transparent', cursor: 'pointer', }, + padding: 0, }), content: css({ paddingTop: theme.spacing(0), diff --git a/public/app/plugins/panel/dashlist/DashListItem.tsx b/public/app/plugins/panel/dashlist/DashListItem.tsx index 6d12eec30e9..cbb093c51f4 100644 --- a/public/app/plugins/panel/dashlist/DashListItem.tsx +++ b/public/app/plugins/panel/dashlist/DashListItem.tsx @@ -1,3 +1,5 @@ +import { truncate } from 'lodash'; + import { reportInteraction } from '@grafana/runtime'; import { Box, Card, Icon, Link, Stack, Text, useStyles2 } from '@grafana/ui'; import { LocationInfo } from 'app/features/search/service/types'; @@ -25,6 +27,7 @@ export function DashListItem({ onStarChange, }: Props) { const css = useStyles2(getStyles); + const shortTitle = truncate(dashboard.name, { length: 40, omission: '…' }); const onCardLinkClick = () => { reportInteraction('grafana_recently_viewed_dashboards_click_card', { @@ -54,27 +57,35 @@ export function DashListItem({ ) : ( - - - {dashboard.name} - - - - - {showFolderNames && locationInfo && ( - - )} diff --git a/public/app/plugins/panel/dashlist/styles.ts b/public/app/plugins/panel/dashlist/styles.ts index c6346480c22..e4197ef03a1 100644 --- a/public/app/plugins/panel/dashlist/styles.ts +++ b/public/app/plugins/panel/dashlist/styles.ts @@ -32,6 +32,7 @@ export const getStyles = (theme: GrafanaTheme2) => { textDecoration: 'underline', }, height: '100%', + paddingTop: theme.spacing(1.5), '&:hover': { backgroundImage: gradient, @@ -41,5 +42,8 @@ export const getStyles = (theme: GrafanaTheme2) => { dashlistCardIcon: css({ marginRight: theme.spacing(0.5), }), + dashlistCardLink: css({ + paddingTop: theme.spacing(0.5), + }), }; }; From ec1ace398e1ed3ae35a358e9dbbe7e6f50f67375 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Tue, 13 Jan 2026 17:22:20 +0200 Subject: [PATCH 08/19] Recent dashboards: Add experimental toggle (#116121) * Add experimentRecentlyViewedDashboards toggle * Emit dashboards_browse_list_viewed event * Move feature toggle to parent * merge --- .../src/types/featureToggles.gen.ts | 5 ++++ pkg/services/featuremgmt/registry.go | 9 +++++++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.json | 15 +++++++++++ .../BrowseDashboardsPage.tsx | 25 ++++++++++++++++--- .../components/RecentlyViewedDashboards.tsx | 6 +---- 6 files changed, 53 insertions(+), 8 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 4850555be08..aa0581004e1 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -984,6 +984,11 @@ export interface FeatureToggles { */ recentlyViewedDashboards?: boolean; /** + * A/A test for recently viewed dashboards feature + * @default false + */ + experimentRecentlyViewedDashboards?: boolean; + /** * Enable configuration of alert enrichments in Grafana Cloud. * @default false */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 64511b3ccfa..a41e6804176 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1625,6 +1625,15 @@ var ( FrontendOnly: true, Expression: "false", }, + { + Name: "experimentRecentlyViewedDashboards", + Description: "A/A test for recently viewed dashboards feature", + Stage: FeatureStageExperimental, + Owner: grafanaFrontendSearchNavOrganise, + FrontendOnly: true, + HideFromDocs: true, + Expression: "false", + }, { Name: "alertEnrichment", Description: "Enable configuration of alert enrichments in Grafana Cloud.", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 9f74c053697..94de5c790c3 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -223,6 +223,7 @@ kubernetesAuthnMutation,experimental,@grafana/identity-access-team,false,false,f kubernetesExternalGroupMapping,experimental,@grafana/identity-access-team,false,false,false restoreDashboards,experimental,@grafana/grafana-search-navigate-organise,false,false,false recentlyViewedDashboards,experimental,@grafana/grafana-search-navigate-organise,false,false,true +experimentRecentlyViewedDashboards,experimental,@grafana/grafana-search-navigate-organise,false,false,true alertEnrichment,experimental,@grafana/alerting-squad,false,false,false alertEnrichmentMultiStep,experimental,@grafana/alerting-squad,false,false,false alertEnrichmentConditional,experimental,@grafana/alerting-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index d96eb8e8d5a..75e3ca69afe 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1365,6 +1365,21 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "experimentRecentlyViewedDashboards", + "resourceVersion": "1768214542023", + "creationTimestamp": "2026-01-12T10:42:22Z" + }, + "spec": { + "description": "A/A test for recently viewed dashboards feature", + "stage": "experimental", + "codeowner": "@grafana/grafana-search-navigate-organise", + "frontend": true, + "hideFromDocs": true, + "expression": "false" + } + }, { "metadata": { "name": "exploreLogsAggregatedMetrics", diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index e177fcf9fa2..1a6b8c65011 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -1,11 +1,12 @@ import { css } from '@emotion/css'; -import { memo, useEffect, useMemo } from 'react'; +import { memo, useEffect, useMemo, useRef } from 'react'; import { useLocation, useParams } from 'react-router-dom-v5-compat'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { config, reportInteraction } from '@grafana/runtime'; +import { evaluateBooleanFlag } from '@grafana/runtime/internal'; import { LinkButton, FilterInput, useStyles2, Text, Stack } from '@grafana/ui'; import { useGetFolderQueryFacade, useUpdateFolder } from 'app/api/clients/folder/v1beta1/hooks'; import { Page } from 'app/core/components/Page/Page'; @@ -44,6 +45,7 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record new URLSearchParams(location.search), [location.search]); const { isReadOnlyRepo, repoType } = useGetResourceRepositoryView({ folderName: folderUID }); + const isRecentlyViewedEnabled = !folderUID && evaluateBooleanFlag('recentlyViewedDashboards', false); useEffect(() => { stateManager.initStateFromUrl(folderUID); @@ -73,6 +75,23 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record { + if (!isRecentlyViewedEnabled || hasEmittedExposureEvent.current) { + return; + } + + hasEmittedExposureEvent.current = true; + const isExperimentTreatment = evaluateBooleanFlag('experimentRecentlyViewedDashboards', false); + + reportInteraction('dashboards_browse_list_viewed', { + experiment_dashboard_list_recently_viewed: isExperimentTreatment ? 'treatment' : 'control', + has_recently_viewed_component: isExperimentTreatment, + }); + }, [isRecentlyViewedEnabled]); + const { data: folderDTO } = useGetFolderQueryFacade(folderUID); const [saveFolder] = useUpdateFolder(); const navModel = useMemo(() => { @@ -179,8 +198,8 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record - {/* only show recently viewed dashboards when in root */} - {!folderUID && } + {/* only show recently viewed dashboards when in root and flag is enabled */} + {isRecentlyViewedEnabled && }
{ - if (!evaluateBooleanFlag('recentlyViewedDashboards', false)) { - return []; - } return getRecentlyViewedDashboards(MAX_RECENT); }, []); const { foldersByUid } = useDashboardLocationInfo(recentDashboards.length > 0); @@ -48,7 +44,7 @@ export function RecentlyViewedDashboards() { setIsOpen(!isOpen); }; - if (!evaluateBooleanFlag('recentlyViewedDashboards', false) || recentDashboards.length === 0) { + if (recentDashboards.length === 0) { return null; } From 1d3f09d5193aff18782b7a8784885c2b8337fb22 Mon Sep 17 00:00:00 2001 From: Alyssa Joyner <58453566+alyssajoyner@users.noreply.github.com> Date: Tue, 13 Jan 2026 08:32:09 -0700 Subject: [PATCH 09/19] [InfluxDB]: Remove banner (#116141) --- .../editor/config-v2/ConfigEditor.test.tsx | 5 ----- .../editor/config-v2/ConfigEditor.tsx | 19 +------------------ 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/components/editor/config-v2/ConfigEditor.test.tsx b/public/app/plugins/datasource/influxdb/components/editor/config-v2/ConfigEditor.test.tsx index 9035fc5c7a8..db40627bc0a 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/config-v2/ConfigEditor.test.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/config-v2/ConfigEditor.test.tsx @@ -36,9 +36,4 @@ describe('ConfigEditor', () => { expect(screen.getByTestId('url-auth-section')).toBeInTheDocument(); expect(screen.getByTestId('db-connection-section')).toBeInTheDocument(); }); - - it('shows the informational alert', () => { - render(); - expect(screen.getByText(/You are viewing a new design/i)).toBeInTheDocument(); - }); }); diff --git a/public/app/plugins/datasource/influxdb/components/editor/config-v2/ConfigEditor.tsx b/public/app/plugins/datasource/influxdb/components/editor/config-v2/ConfigEditor.tsx index a6cc7eb3747..c68b7f2c039 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/config-v2/ConfigEditor.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/config-v2/ConfigEditor.tsx @@ -2,13 +2,12 @@ import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Alert, Box, Stack, TextLink, Text, useStyles2 } from '@grafana/ui'; +import { Box, Stack, Text, useStyles2 } from '@grafana/ui'; import { DatabaseConnectionSection } from './DatabaseConnectionSection'; import { LeftSideBar } from './LeftSideBar'; import { UrlAndAuthenticationSection } from './UrlAndAuthenticationSection'; import { CONTAINER_MIN_WIDTH } from './constants'; -import { trackInfluxDBConfigV2FeedbackButtonClicked } from './tracking'; import { Props } from './types'; export const ConfigEditor: React.FC = ({ onOptionsChange, options }: Props) => { @@ -22,22 +21,6 @@ export const ConfigEditor: React.FC = ({ onOptionsChange, options }: Prop
- - <> - - Share your thoughts - {' '} - to help us make it even better. - - Fields marked with * are required From b687ca6b6d9f641835d97fdd90a0d1ac33f48d19 Mon Sep 17 00:00:00 2001 From: Motte <37443982+dmotte@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:48:50 +0100 Subject: [PATCH 10/19] Chore: Improve packaging/docker/run.sh (#114012) * Chore: set -e line in packaging/docker/run.sh * Chore: fix ShellCheck SC2188 in packaging/docker/run.sh * Chore: fix ShellCheck SC2166 in packaging/docker/run.sh --- packaging/docker/run.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packaging/docker/run.sh b/packaging/docker/run.sh index a4c91b49379..148d7ccb30f 100755 --- a/packaging/docker/run.sh +++ b/packaging/docker/run.sh @@ -1,4 +1,5 @@ -#!/bin/bash -e +#!/bin/bash +set -e PERMISSIONS_OK=0 @@ -26,14 +27,14 @@ if [ ! -d "$GF_PATHS_PLUGINS" ]; then fi if [ ! -z ${GF_AWS_PROFILES+x} ]; then - > "$GF_PATHS_HOME/.aws/credentials" + :> "$GF_PATHS_HOME/.aws/credentials" for profile in ${GF_AWS_PROFILES}; do access_key_varname="GF_AWS_${profile}_ACCESS_KEY_ID" secret_key_varname="GF_AWS_${profile}_SECRET_ACCESS_KEY" region_varname="GF_AWS_${profile}_REGION" - if [ ! -z "${!access_key_varname}" -a ! -z "${!secret_key_varname}" ]; then + if [ ! -z "${!access_key_varname}" ] && [ ! -z "${!secret_key_varname}" ]; then echo "[${profile}]" >> "$GF_PATHS_HOME/.aws/credentials" echo "aws_access_key_id = ${!access_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials" echo "aws_secret_access_key = ${!secret_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials" From a28076ef5ea0c681b356220856c3f5c1ce818a22 Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:41:35 -0600 Subject: [PATCH 11/19] Logs: Feature flag clean up (#116205) * chore: reassign flags to big tent --- pkg/services/featuremgmt/registry.go | 10 ++--- pkg/services/featuremgmt/toggles_gen.csv | 10 ++--- pkg/services/featuremgmt/toggles_gen.json | 45 +++++++++++++++-------- 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index a41e6804176..4c0456e9457 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -49,7 +49,7 @@ var ( Name: "lokiExperimentalStreaming", Description: "Support new streaming approach for loki (prototype, needs special loki build)", Stage: FeatureStageExperimental, - Owner: grafanaObservabilityLogsSquad, + Owner: grafanaOSSBigTent, }, { Name: "featureHighlights", @@ -177,7 +177,7 @@ var ( Name: "lokiLogsDataplane", Description: "Changes logs responses from Loki to be compliant with the dataplane specification.", Stage: FeatureStageExperimental, - Owner: grafanaObservabilityLogsSquad, + Owner: grafanaOSSBigTent, }, { Name: "disableSSEDataplane", @@ -340,7 +340,7 @@ var ( Description: "Enables running Loki queries in parallel", Stage: FeatureStagePrivatePreview, FrontendOnly: false, - Owner: grafanaObservabilityLogsSquad, + Owner: grafanaOSSBigTent, }, { Name: "externalServiceAccounts", @@ -745,7 +745,7 @@ var ( Name: "logQLScope", Description: "In-development feature that will allow injection of labels into loki queries.", Stage: FeatureStagePrivatePreview, - Owner: grafanaObservabilityLogsSquad, + Owner: grafanaOSSBigTent, Expression: "false", HideFromDocs: true, }, @@ -1260,7 +1260,7 @@ var ( Name: "lokiLabelNamesQueryApi", Description: "Defaults to using the Loki `/labels` API instead of `/series`", Stage: FeatureStageGeneralAvailability, - Owner: grafanaObservabilityLogsSquad, + Owner: grafanaOSSBigTent, Expression: "true", }, { diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 94de5c790c3..caba7cdab90 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -3,7 +3,7 @@ disableEnvelopeEncryption,GA,@grafana/grafana-operator-experience-squad,false,fa panelTitleSearch,preview,@grafana/search-and-storage,false,false,false publicDashboardsEmailSharing,preview,@grafana/grafana-operator-experience-squad,false,false,false publicDashboardsScene,GA,@grafana/grafana-operator-experience-squad,false,false,true -lokiExperimentalStreaming,experimental,@grafana/observability-logs,false,false,false +lokiExperimentalStreaming,experimental,@grafana/oss-big-tent,false,false,false featureHighlights,GA,@grafana/grafana-operator-experience-squad,false,false,false storage,experimental,@grafana/search-and-storage,false,false,false canvasPanelNesting,experimental,@grafana/dataviz-squad,false,false,true @@ -22,7 +22,7 @@ starsFromAPIServer,experimental,@grafana/grafana-search-navigate-organise,false, kubernetesStars,experimental,@grafana/grafana-app-platform-squad,false,true,false influxqlStreamingParser,experimental,@grafana/partner-datasources,false,false,false influxdbRunQueriesInParallel,privatePreview,@grafana/partner-datasources,false,false,false -lokiLogsDataplane,experimental,@grafana/observability-logs,false,false,false +lokiLogsDataplane,experimental,@grafana/oss-big-tent,false,false,false disableSSEDataplane,experimental,@grafana/grafana-datasources-core-services,false,false,false renderAuthJWT,preview,@grafana/grafana-operator-experience-squad,false,false,false refactorVariablesTimeRange,preview,@grafana/dashboards-squad,false,false,false @@ -45,7 +45,7 @@ aiGeneratedDashboardChanges,experimental,@grafana/dashboards-squad,false,false,t reportingRetries,preview,@grafana/grafana-operator-experience-squad,false,true,false reportingCsvEncodingOptions,experimental,@grafana/grafana-operator-experience-squad,false,false,false sseGroupByDatasource,experimental,@grafana/grafana-datasources-core-services,false,false,false -lokiRunQueriesInParallel,privatePreview,@grafana/observability-logs,false,false,false +lokiRunQueriesInParallel,privatePreview,@grafana/oss-big-tent,false,false,false externalServiceAccounts,preview,@grafana/identity-access-team,false,false,false enableNativeHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false disableClassicHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false @@ -102,7 +102,7 @@ alertingSaveStateCompressed,preview,@grafana/alerting-squad,false,false,false scopeApi,experimental,@grafana/grafana-app-platform-squad,false,false,false useScopeSingleNodeEndpoint,experimental,@grafana/grafana-operator-experience-squad,false,false,true useMultipleScopeNodesEndpoint,experimental,@grafana/grafana-operator-experience-squad,false,false,true -logQLScope,privatePreview,@grafana/observability-logs,false,false,false +logQLScope,privatePreview,@grafana/oss-big-tent,false,false,false sqlExpressions,preview,@grafana/grafana-datasources-core-services,false,false,false sqlExpressionsColumnAutoComplete,experimental,@grafana/datapro,false,false,true kubernetesAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false @@ -173,7 +173,7 @@ alertingAIAnalyzeCentralStateHistory,experimental,@grafana/alerting-squad,false, alertingNotificationsStepMode,GA,@grafana/alerting-squad,false,false,true unifiedStorageSearchUI,experimental,@grafana/search-and-storage,false,false,false elasticsearchCrossClusterSearch,GA,@grafana/partner-datasources,false,false,false -lokiLabelNamesQueryApi,GA,@grafana/observability-logs,false,false,false +lokiLabelNamesQueryApi,GA,@grafana/oss-big-tent,false,false,false k8SFolderCounts,experimental,@grafana/search-and-storage,false,false,false k8SFolderMove,experimental,@grafana/search-and-storage,false,false,false improvedExternalSessionHandlingSAML,GA,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 75e3ca69afe..bfefc20f08b 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2222,13 +2222,16 @@ { "metadata": { "name": "logQLScope", - "resourceVersion": "1764664939750", - "creationTimestamp": "2024-11-11T11:53:24Z" + "resourceVersion": "1768317398145", + "creationTimestamp": "2024-11-11T11:53:24Z", + "annotations": { + "grafana.app/updatedTimestamp": "2026-01-13 15:16:38.145488 +0000 UTC" + } }, "spec": { "description": "In-development feature that will allow injection of labels into loki queries.", "stage": "privatePreview", - "codeowner": "@grafana/observability-logs", + "codeowner": "@grafana/oss-big-tent", "hideFromDocs": true, "expression": "false" } @@ -2304,38 +2307,47 @@ { "metadata": { "name": "lokiExperimentalStreaming", - "resourceVersion": "1764664939750", - "creationTimestamp": "2023-06-19T10:03:51Z" + "resourceVersion": "1768317398145", + "creationTimestamp": "2023-06-19T10:03:51Z", + "annotations": { + "grafana.app/updatedTimestamp": "2026-01-13 15:16:38.145488 +0000 UTC" + } }, "spec": { "description": "Support new streaming approach for loki (prototype, needs special loki build)", "stage": "experimental", - "codeowner": "@grafana/observability-logs" + "codeowner": "@grafana/oss-big-tent" } }, { "metadata": { "name": "lokiLabelNamesQueryApi", - "resourceVersion": "1764664939750", - "creationTimestamp": "2024-12-13T14:31:41Z" + "resourceVersion": "1768317398145", + "creationTimestamp": "2024-12-13T14:31:41Z", + "annotations": { + "grafana.app/updatedTimestamp": "2026-01-13 15:16:38.145488 +0000 UTC" + } }, "spec": { "description": "Defaults to using the Loki `/labels` API instead of `/series`", "stage": "GA", - "codeowner": "@grafana/observability-logs", + "codeowner": "@grafana/oss-big-tent", "expression": "true" } }, { "metadata": { "name": "lokiLogsDataplane", - "resourceVersion": "1764664939750", - "creationTimestamp": "2023-07-13T07:58:00Z" + "resourceVersion": "1768317398145", + "creationTimestamp": "2023-07-13T07:58:00Z", + "annotations": { + "grafana.app/updatedTimestamp": "2026-01-13 15:16:38.145488 +0000 UTC" + } }, "spec": { "description": "Changes logs responses from Loki to be compliant with the dataplane specification.", "stage": "experimental", - "codeowner": "@grafana/observability-logs" + "codeowner": "@grafana/oss-big-tent" } }, { @@ -2368,13 +2380,16 @@ { "metadata": { "name": "lokiRunQueriesInParallel", - "resourceVersion": "1764664939750", - "creationTimestamp": "2023-09-19T09:34:01Z" + "resourceVersion": "1768317398145", + "creationTimestamp": "2023-09-19T09:34:01Z", + "annotations": { + "grafana.app/updatedTimestamp": "2026-01-13 15:16:38.145488 +0000 UTC" + } }, "spec": { "description": "Enables running Loki queries in parallel", "stage": "privatePreview", - "codeowner": "@grafana/observability-logs" + "codeowner": "@grafana/oss-big-tent" } }, { From 6186aac5d4fc35f9b0bd64200ff4481d3aa2c456 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Tue, 13 Jan 2026 18:34:39 +0000 Subject: [PATCH 12/19] Revert "Plugins: Add module hash field to plugin model" (#116211) * Revert "Plugins: Add module hash field to plugin model (#116119)" This reverts commit aa9b587cc1e50bcbfe31d78e98473c78d451bf67. * trigger * trigger --- apps/plugins/pkg/app/meta/local.go | 7 +- pkg/api/frontendsettings.go | 6 +- pkg/api/frontendsettings_test.go | 7 +- pkg/api/plugins.go | 2 +- pkg/api/plugins_test.go | 5 +- pkg/plugins/ifaces.go | 4 +- pkg/plugins/manager/loader/loader_test.go | 25 +- .../manager/pipeline/bootstrap/bootstrap.go | 3 +- .../manager/pipeline/bootstrap/steps.go | 27 +- pkg/plugins/manager/signature/manifest.go | 55 ++- .../manager/signature/manifest_test.go | 32 +- pkg/plugins/pluginassets/modulehash.go | 90 ----- pkg/plugins/pluginassets/modulehash_test.go | 356 ----------------- pkg/plugins/plugins.go | 27 +- pkg/server/wire_gen.go | 12 +- .../pluginsintegration/loader/loader_test.go | 206 ++-------- .../pluginsintegration/pipeline/pipeline.go | 5 +- .../pluginassets/pluginassets.go | 132 +++++- .../pluginassets/pluginassets_test.go | 375 ++++++++++++++++++ .../module-hash-no-manifest-txt/module.js | 0 .../module-hash-no-manifest-txt/plugin.json | 0 .../module-hash-no-module-js/MANIFEST.txt | 0 .../module-hash-no-module-js/plugin.json | 0 .../module-hash-no-module-js/something.js | 0 .../MANIFEST.txt | 0 .../datasource/module.js | 0 .../datasource/panels/one/module.js | 0 .../datasource/panels/one/plugin.json | 0 .../datasource/plugin.json | 0 .../module-hash-valid-deeply-nested/module.js | 0 .../plugin.json | 0 .../module-hash-valid-nested/MANIFEST.txt | 0 .../datasource/module.js | 0 .../datasource/plugin.json | 0 .../module-hash-valid-nested/module.js | 0 .../panels/one/module.js | 0 .../panels/one/plugin.json | 0 .../module-hash-valid-nested/plugin.json | 0 .../testdata/module-hash-valid/MANIFEST.txt | 0 .../testdata/module-hash-valid/module.js | 0 .../testdata/module-hash-valid/plugin.json | 0 .../pluginsintegration/pluginstore/plugins.go | 6 +- .../pluginsintegration/test_helper.go | 5 +- 43 files changed, 620 insertions(+), 767 deletions(-) delete mode 100644 pkg/plugins/pluginassets/modulehash.go delete mode 100644 pkg/plugins/pluginassets/modulehash_test.go rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-no-manifest-txt/module.js (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-no-manifest-txt/plugin.json (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-no-module-js/MANIFEST.txt (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-no-module-js/plugin.json (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-no-module-js/something.js (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-valid-deeply-nested/MANIFEST.txt (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/module.js (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/module.js (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/plugin.json (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/plugin.json (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-valid-deeply-nested/module.js (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-valid-deeply-nested/plugin.json (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-valid-nested/MANIFEST.txt (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-valid-nested/datasource/module.js (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-valid-nested/datasource/plugin.json (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-valid-nested/module.js (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-valid-nested/panels/one/module.js (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-valid-nested/panels/one/plugin.json (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-valid-nested/plugin.json (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-valid/MANIFEST.txt (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-valid/module.js (100%) rename pkg/{plugins => services/pluginsintegration}/pluginassets/testdata/module-hash-valid/plugin.json (100%) diff --git a/apps/plugins/pkg/app/meta/local.go b/apps/plugins/pkg/app/meta/local.go index af40316cd73..2c699520cfc 100644 --- a/apps/plugins/pkg/app/meta/local.go +++ b/apps/plugins/pkg/app/meta/local.go @@ -13,9 +13,10 @@ const ( ) // PluginAssetsCalculator is an interface for calculating plugin asset information. -// LocalProvider requires this to calculate loading strategy. +// LocalProvider requires this to calculate loading strategy and module hash. type PluginAssetsCalculator interface { LoadingStrategy(ctx context.Context, p pluginstore.Plugin) plugins.LoadingStrategy + ModuleHash(ctx context.Context, p pluginstore.Plugin) string } // LocalProvider retrieves plugin metadata for locally installed plugins. @@ -26,7 +27,7 @@ type LocalProvider struct { } // NewLocalProvider creates a new LocalProvider for locally installed plugins. -// pluginAssets is required for calculating loading strategy. +// pluginAssets is required for calculating loading strategy and module hash. func NewLocalProvider(pluginStore pluginstore.Store, pluginAssets PluginAssetsCalculator) *LocalProvider { return &LocalProvider{ store: pluginStore, @@ -42,7 +43,7 @@ func (p *LocalProvider) GetMeta(ctx context.Context, pluginID, version string) ( } loadingStrategy := p.pluginAssets.LoadingStrategy(ctx, plugin) - moduleHash := plugin.ModuleHash + moduleHash := p.pluginAssets.ModuleHash(ctx, plugin) spec := pluginStorePluginToMeta(plugin, loadingStrategy, moduleHash) return &Result{ diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 64165cfb98a..b57262087e5 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -161,7 +161,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro AliasIDs: panel.AliasIDs, Info: panel.Info, Module: panel.Module, - ModuleHash: panel.ModuleHash, + ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), panel), BaseURL: panel.BaseURL, SkipDataQuery: panel.SkipDataQuery, Suggestions: panel.Suggestions, @@ -527,7 +527,7 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug JSONData: plugin.JSONData, Signature: plugin.Signature, Module: plugin.Module, - ModuleHash: plugin.ModuleHash, + ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), plugin), BaseURL: plugin.BaseURL, Angular: plugin.Angular, MultiValueFilterOperators: plugin.MultiValueFilterOperators, @@ -641,7 +641,7 @@ func (hs *HTTPServer) newAppDTO(ctx context.Context, plugin pluginstore.Plugin, LoadingStrategy: hs.pluginAssets.LoadingStrategy(ctx, plugin), Extensions: plugin.Extensions, Dependencies: plugin.Dependencies, - ModuleHash: plugin.ModuleHash, + ModuleHash: hs.pluginAssets.ModuleHash(ctx, plugin), Translations: plugin.Translations, BuildMode: plugin.BuildMode, } diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index 4741f6b10bc..ba67837be5d 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -20,6 +20,8 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" + "github.com/grafana/grafana/pkg/plugins/manager/signature" + "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/pluginscdn" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -77,7 +79,8 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F var pluginsAssets = passets if pluginsAssets == nil { - pluginsAssets = pluginassets.ProvideService(pluginsCfg, pluginsCDN, pluginStore) + sig := signature.ProvideService(pluginsCfg, statickey.New()) + pluginsAssets = pluginassets.ProvideService(pluginsCfg, pluginsCDN, sig, pluginStore) } hs := &HTTPServer{ @@ -711,6 +714,6 @@ func newPluginAssets() func() *pluginassets.Service { func newPluginAssetsWithConfig(pCfg *config.PluginManagementCfg) func() *pluginassets.Service { return func() *pluginassets.Service { - return pluginassets.ProvideService(pCfg, pluginscdn.ProvideService(pCfg), &pluginstore.FakePluginStore{}) + return pluginassets.ProvideService(pCfg, pluginscdn.ProvideService(pCfg), signature.ProvideService(pCfg, statickey.New()), &pluginstore.FakePluginStore{}) } } diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 079dae066bb..50e32326565 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -201,7 +201,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response. Includes: plugin.Includes, BaseUrl: plugin.BaseURL, Module: plugin.Module, - ModuleHash: plugin.ModuleHash, + ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), plugin), DefaultNavUrl: path.Join(hs.Cfg.AppSubURL, plugin.DefaultNavURL), State: plugin.State, Signature: plugin.Signature, diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index 238b0b3390f..342c6293d7e 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -28,6 +28,8 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/filestore" "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" "github.com/grafana/grafana/pkg/plugins/manager/registry" + "github.com/grafana/grafana/pkg/plugins/manager/signature" + "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/pluginerrs" "github.com/grafana/grafana/pkg/plugins/pluginscdn" ac "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -846,7 +848,8 @@ func Test_PluginsSettings(t *testing.T) { } pCfg := &config.PluginManagementCfg{} pluginCDN := pluginscdn.ProvideService(pCfg) - hs.pluginAssets = pluginassets.ProvideService(pCfg, pluginCDN, hs.pluginStore) + sig := signature.ProvideService(pCfg, statickey.New()) + hs.pluginAssets = pluginassets.ProvideService(pCfg, pluginCDN, sig, hs.pluginStore) hs.pluginErrorResolver = pluginerrs.ProvideStore(errTracker) hs.pluginsUpdateChecker, err = updatemanager.ProvidePluginsService( hs.Cfg, diff --git a/pkg/plugins/ifaces.go b/pkg/plugins/ifaces.go index 1d33d34e6c9..9b719b8b180 100644 --- a/pkg/plugins/ifaces.go +++ b/pkg/plugins/ifaces.go @@ -140,9 +140,7 @@ type Licensing interface { } type SignatureCalculator interface { - // Calculate calculates the signature and returns both the signature and the manifest. - // The manifest may be nil if the plugin is unsigned or if an error occurred. - Calculate(ctx context.Context, src PluginSource, plugin FoundPlugin) (Signature, *PluginManifest, error) + Calculate(ctx context.Context, src PluginSource, plugin FoundPlugin) (Signature, error) } type KeyStore interface { diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index f4c63e71e15..7613392f497 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -216,27 +216,10 @@ func TestLoader_Load(t *testing.T) { ExtensionPoints: []plugins.ExtensionPoint{}, }, }, - Class: plugins.ClassExternal, - Module: "public/plugins/test-app/module.js", - BaseURL: "public/plugins/test-app", - FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/includes-symlinks")), - Manifest: &plugins.PluginManifest{ - Plugin: "test-app", - Version: "1.0.0", - KeyID: "7e4d0c6a708866e7", - Time: 1622547655175, - Files: map[string]string{ - "dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303", - "dashboards/extra/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee", - "plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf", - "symlink_to_txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048", - "text.txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048", - }, - ManifestVersion: "2.0.0", - SignatureType: plugins.SignatureTypeGrafana, - SignedByOrg: "grafana", - SignedByOrgName: "Grafana Labs", - }, + Class: plugins.ClassExternal, + Module: "public/plugins/test-app/module.js", + BaseURL: "public/plugins/test-app", + FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/includes-symlinks")), Signature: "valid", SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", diff --git a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go index c129e203dc5..f20c1ff1ead 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go +++ b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/pluginassets" - "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/tracing" "github.com/grafana/grafana/pkg/semconv" ) @@ -55,7 +54,7 @@ func New(cfg *config.PluginManagementCfg, opts Opts) *Bootstrap { } if opts.DecorateFuncs == nil { - opts.DecorateFuncs = DefaultDecorateFuncs(cfg, pluginscdn.ProvideService(cfg)) + opts.DecorateFuncs = DefaultDecorateFuncs(cfg) } return &Bootstrap{ diff --git a/pkg/plugins/manager/pipeline/bootstrap/steps.go b/pkg/plugins/manager/pipeline/bootstrap/steps.go index 5ab85dc6e43..5c365ebb47c 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/steps.go +++ b/pkg/plugins/manager/pipeline/bootstrap/steps.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/pluginassets" - "github.com/grafana/grafana/pkg/plugins/pluginscdn" ) // DefaultConstructor implements the default ConstructFunc used for the Construct step of the Bootstrap stage. @@ -29,13 +28,12 @@ func DefaultConstructFunc(cfg *config.PluginManagementCfg, signatureCalculator p } // DefaultDecorateFuncs are the default DecorateFuncs used for the Decorate step of the Bootstrap stage. -func DefaultDecorateFuncs(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service) []DecorateFunc { +func DefaultDecorateFuncs(cfg *config.PluginManagementCfg) []DecorateFunc { return []DecorateFunc{ AppDefaultNavURLDecorateFunc, TemplateDecorateFunc, AppChildDecorateFunc(), SkipHostEnvVarsDecorateFunc(cfg), - ModuleHashDecorateFunc(cfg, cdn), } } @@ -50,30 +48,19 @@ func NewDefaultConstructor(cfg *config.PluginManagementCfg, signatureCalculator // Construct will calculate the plugin's signature state and create the plugin using the pluginFactoryFunc. func (c *DefaultConstructor) Construct(ctx context.Context, src plugins.PluginSource, bundle *plugins.FoundBundle) ([]*plugins.Plugin, error) { - // Calculate signature and cache manifest - sig, manifest, err := c.signatureCalculator.Calculate(ctx, src, bundle.Primary) + sig, err := c.signatureCalculator.Calculate(ctx, src, bundle.Primary) if err != nil { c.log.Warn("Could not calculate plugin signature state", "pluginId", bundle.Primary.JSONData.ID, "error", err) return nil, err } - plugin, err := c.pluginFactoryFunc(bundle, src.PluginClass(ctx), sig) if err != nil { c.log.Error("Could not create primary plugin base", "pluginId", bundle.Primary.JSONData.ID, "error", err) return nil, err } - - plugin.Manifest = manifest - res := make([]*plugins.Plugin, 0, len(plugin.Children)+1) res = append(res, plugin) - for _, child := range plugin.Children { - // Child plugins use the parent's manifest - if child.Parent != nil && child.Parent.Manifest != nil { - child.Manifest = child.Parent.Manifest - } - res = append(res, child) - } + res = append(res, plugin.Children...) return res, nil } @@ -158,11 +145,3 @@ func SkipHostEnvVarsDecorateFunc(cfg *config.PluginManagementCfg) DecorateFunc { return p, nil } } - -// ModuleHashDecorateFunc returns a DecorateFunc that calculates and sets the module hash for the plugin. -func ModuleHashDecorateFunc(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service) DecorateFunc { - return func(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { - p.ModuleHash = pluginassets.CalculateModuleHash(p, cfg, cdn) - return p, nil - } -} diff --git a/pkg/plugins/manager/signature/manifest.go b/pkg/plugins/manager/signature/manifest.go index 4b6bc531d89..6d790c79873 100644 --- a/pkg/plugins/manager/signature/manifest.go +++ b/pkg/plugins/manager/signature/manifest.go @@ -14,6 +14,7 @@ import ( "path" "path/filepath" "runtime" + "strings" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/clearsign" @@ -36,6 +37,26 @@ var ( fromSlash = filepath.FromSlash ) +// PluginManifest holds details for the file manifest +type PluginManifest struct { + Plugin string `json:"plugin"` + Version string `json:"version"` + KeyID string `json:"keyId"` + Time int64 `json:"time"` + Files map[string]string `json:"files"` + + // V2 supported fields + ManifestVersion string `json:"manifestVersion"` + SignatureType plugins.SignatureType `json:"signatureType"` + SignedByOrg string `json:"signedByOrg"` + SignedByOrgName string `json:"signedByOrgName"` + RootURLs []string `json:"rootUrls"` +} + +func (m *PluginManifest) IsV2() bool { + return strings.HasPrefix(m.ManifestVersion, "2.") +} + type Signature struct { kr plugins.KeyRetriever cfg *config.PluginManagementCfg @@ -66,14 +87,14 @@ func DefaultCalculator(cfg *config.PluginManagementCfg) *Signature { // readPluginManifest attempts to read and verify the plugin manifest // if any error occurs or the manifest is not valid, this will return an error -func (s *Signature) readPluginManifest(ctx context.Context, body []byte) (*plugins.PluginManifest, error) { +func (s *Signature) readPluginManifest(ctx context.Context, body []byte) (*PluginManifest, error) { block, _ := clearsign.Decode(body) if block == nil { return nil, errors.New("unable to decode manifest") } // Convert to a well typed object - var manifest plugins.PluginManifest + var manifest PluginManifest err := json.Unmarshal(block.Plaintext, &manifest) if err != nil { return nil, fmt.Errorf("%v: %w", "Error parsing manifest JSON", err) @@ -90,7 +111,7 @@ var ErrSignatureTypeUnsigned = errors.New("plugin is unsigned") // ReadPluginManifestFromFS reads the plugin manifest from the provided plugins.FS. // If the manifest is not found, it will return an error wrapping ErrSignatureTypeUnsigned. -func (s *Signature) ReadPluginManifestFromFS(ctx context.Context, pfs plugins.FS) (*plugins.PluginManifest, error) { +func (s *Signature) ReadPluginManifestFromFS(ctx context.Context, pfs plugins.FS) (*PluginManifest, error) { f, err := pfs.Open("MANIFEST.txt") if err != nil { if errors.Is(err, plugins.ErrFileNotExist) { @@ -119,9 +140,9 @@ func (s *Signature) ReadPluginManifestFromFS(ctx context.Context, pfs plugins.FS return manifest, nil } -func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plugin plugins.FoundPlugin) (plugins.Signature, *plugins.PluginManifest, error) { +func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plugin plugins.FoundPlugin) (plugins.Signature, error) { if defaultSignature, exists := src.DefaultSignature(ctx, plugin.JSONData.ID); exists { - return defaultSignature, nil, nil + return defaultSignature, nil } manifest, err := s.ReadPluginManifestFromFS(ctx, plugin.FS) @@ -130,29 +151,29 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu s.log.Warn("Plugin is unsigned", "id", plugin.JSONData.ID, "err", err) return plugins.Signature{ Status: plugins.SignatureStatusUnsigned, - }, nil, nil + }, nil case err != nil: s.log.Warn("Plugin signature is invalid", "id", plugin.JSONData.ID, "err", err) return plugins.Signature{ Status: plugins.SignatureStatusInvalid, - }, nil, nil + }, nil } if !manifest.IsV2() { return plugins.Signature{ Status: plugins.SignatureStatusInvalid, - }, nil, nil + }, nil } fsFiles, err := plugin.FS.Files() if err != nil { - return plugins.Signature{}, nil, fmt.Errorf("files: %w", err) + return plugins.Signature{}, fmt.Errorf("files: %w", err) } if len(fsFiles) == 0 { s.log.Warn("No plugin file information in directory", "pluginId", plugin.JSONData.ID) return plugins.Signature{ Status: plugins.SignatureStatusInvalid, - }, nil, nil + }, nil } // Make sure the versions all match @@ -160,20 +181,20 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu s.log.Debug("Plugin signature invalid because ID or Version mismatch", "pluginId", plugin.JSONData.ID, "manifestPluginId", manifest.Plugin, "pluginVersion", plugin.JSONData.Info.Version, "manifestPluginVersion", manifest.Version) return plugins.Signature{ Status: plugins.SignatureStatusModified, - }, nil, nil + }, nil } // Validate that plugin is running within defined root URLs if len(manifest.RootURLs) > 0 { if match, err := urlMatch(manifest.RootURLs, s.cfg.GrafanaAppURL, manifest.SignatureType); err != nil { s.log.Warn("Could not verify if root URLs match", "plugin", plugin.JSONData.ID, "rootUrls", manifest.RootURLs) - return plugins.Signature{}, nil, err + return plugins.Signature{}, err } else if !match { s.log.Warn("Could not find root URL that matches running application URL", "plugin", plugin.JSONData.ID, "appUrl", s.cfg.GrafanaAppURL, "rootUrls", manifest.RootURLs) return plugins.Signature{ Status: plugins.SignatureStatusInvalid, - }, nil, nil + }, nil } } @@ -186,7 +207,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu s.log.Debug("Plugin signature invalid", "pluginId", plugin.JSONData.ID, "error", err) return plugins.Signature{ Status: plugins.SignatureStatusModified, - }, nil, nil + }, nil } manifestFiles[p] = struct{}{} @@ -215,7 +236,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu s.log.Warn("The following files were not included in the signature", "plugin", plugin.JSONData.ID, "files", unsignedFiles) return plugins.Signature{ Status: plugins.SignatureStatusModified, - }, nil, nil + }, nil } s.log.Debug("Plugin signature valid", "id", plugin.JSONData.ID) @@ -223,7 +244,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu Status: plugins.SignatureStatusValid, Type: manifest.SignatureType, SigningOrg: manifest.SignedByOrgName, - }, manifest, nil + }, nil } func verifyHash(mlog log.Logger, plugin plugins.FoundPlugin, path, hash string) error { @@ -300,7 +321,7 @@ func (r invalidFieldErr) Error() string { return fmt.Sprintf("valid manifest field %s is required", r.field) } -func (s *Signature) validateManifest(ctx context.Context, m plugins.PluginManifest, block *clearsign.Block) error { +func (s *Signature) validateManifest(ctx context.Context, m PluginManifest, block *clearsign.Block) error { if len(m.Plugin) == 0 { return invalidFieldErr{field: "plugin"} } diff --git a/pkg/plugins/manager/signature/manifest_test.go b/pkg/plugins/manager/signature/manifest_test.go index 5768f0f32f5..d399268b464 100644 --- a/pkg/plugins/manager/signature/manifest_test.go +++ b/pkg/plugins/manager/signature/manifest_test.go @@ -164,7 +164,7 @@ func TestCalculate(t *testing.T) { for _, tc := range tcs { basePath := filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin") s := provideTestServiceWithConfig(&config.PluginManagementCfg{GrafanaAppURL: tc.appURL}) - sig, _, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ + sig, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.ClassExternal }, @@ -192,7 +192,7 @@ func TestCalculate(t *testing.T) { runningWindows = true s := provideDefaultTestService() - sig, _, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ + sig, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.ClassExternal }, @@ -260,7 +260,7 @@ func TestCalculate(t *testing.T) { require.NoError(t, err) pfs, err = newPathSeparatorOverrideFS(string(tc.platform.separator), pfs) require.NoError(t, err) - sig, _, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ + sig, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.ClassExternal }, @@ -396,7 +396,7 @@ func TestFSPathSeparatorFiles(t *testing.T) { } } -func fileList(manifest *plugins.PluginManifest) []string { +func fileList(manifest *PluginManifest) []string { keys := make([]string, 0, len(manifest.Files)) for k := range manifest.Files { keys = append(keys, k) @@ -682,52 +682,52 @@ func Test_urlMatch_private(t *testing.T) { func Test_validateManifest(t *testing.T) { tcs := []struct { name string - manifest *plugins.PluginManifest + manifest *PluginManifest expectedErr string }{ { name: "Empty plugin field", - manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.Plugin = "" }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.Plugin = "" }), expectedErr: "valid manifest field plugin is required", }, { name: "Empty keyId field", - manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.KeyID = "" }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.KeyID = "" }), expectedErr: "valid manifest field keyId is required", }, { name: "Empty signedByOrg field", - manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.SignedByOrg = "" }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignedByOrg = "" }), expectedErr: "valid manifest field signedByOrg is required", }, { name: "Empty signedByOrgName field", - manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.SignedByOrgName = "" }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignedByOrgName = "" }), expectedErr: "valid manifest field SignedByOrgName is required", }, { name: "Empty signatureType field", - manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.SignatureType = "" }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignatureType = "" }), expectedErr: "valid manifest field signatureType is required", }, { name: "Invalid signatureType field", - manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.SignatureType = "invalidSignatureType" }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignatureType = "invalidSignatureType" }), expectedErr: "valid manifest field signatureType is required", }, { name: "Empty files field", - manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.Files = map[string]string{} }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.Files = map[string]string{} }), expectedErr: "valid manifest field files is required", }, { name: "Empty time field", - manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.Time = 0 }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.Time = 0 }), expectedErr: "valid manifest field time is required", }, { name: "Empty version field", - manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.Version = "" }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.Version = "" }), expectedErr: "valid manifest field version is required", }, } @@ -740,10 +740,10 @@ func Test_validateManifest(t *testing.T) { } } -func createV2Manifest(t *testing.T, cbs ...func(*plugins.PluginManifest)) *plugins.PluginManifest { +func createV2Manifest(t *testing.T, cbs ...func(*PluginManifest)) *PluginManifest { t.Helper() - m := &plugins.PluginManifest{ + m := &PluginManifest{ Plugin: "grafana-test-app", Version: "2.5.3", KeyID: "7e4d0c6a708866e7", diff --git a/pkg/plugins/pluginassets/modulehash.go b/pkg/plugins/pluginassets/modulehash.go deleted file mode 100644 index fd1ff13c578..00000000000 --- a/pkg/plugins/pluginassets/modulehash.go +++ /dev/null @@ -1,90 +0,0 @@ -package pluginassets - -import ( - "encoding/base64" - "encoding/hex" - "path" - "path/filepath" - - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/config" - "github.com/grafana/grafana/pkg/plugins/pluginscdn" -) - -// CalculateModuleHash calculates the module.js SHA256 hash for a plugin in the format expected by the browser for SRI checks. -// The module hash is read from the plugin's cached manifest. -// For nested plugins, the module hash is read from the root parent plugin's manifest. -// If the plugin is unsigned or not a CDN plugin, an empty string is returned. -func CalculateModuleHash(p *plugins.Plugin, cfg *config.PluginManagementCfg, cdn *pluginscdn.Service) string { - if cfg == nil || !cfg.Features.SriChecksEnabled { - return "" - } - - if !p.Signature.IsValid() { - return "" - } - - rootParent := findRootParent(p) - if rootParent.Manifest == nil { - return "" - } - - if !rootParent.Manifest.IsV2() { - return "" - } - - if !cdnEnabled(rootParent, cdn) { - return "" - } - - modulePath := getModulePathInManifest(p, rootParent) - moduleHash, ok := rootParent.Manifest.Files[modulePath] - if !ok { - return "" - } - - return convertHashForSRI(moduleHash) -} - -// findRootParent returns the root parent plugin (the one that contains the manifest). -// For non-nested plugins, it returns the plugin itself. -func findRootParent(p *plugins.Plugin) *plugins.Plugin { - root := p - for root.Parent != nil { - root = root.Parent - } - return root -} - -// getModulePathInManifest returns the path to module.js as it appears in the manifest. -// For nested plugins, this is the relative path from the root parent to the plugin's module.js. -// For non-nested plugins, this is simply "module.js". -func getModulePathInManifest(p *plugins.Plugin, rootParent *plugins.Plugin) string { - if p == rootParent { - return "module.js" - } - - // Calculate the relative path from root parent to this plugin - relPath, err := rootParent.FS.Rel(p.FS.Base()) - if err != nil { - return "" - } - - // MANIFEST.txt uses forward slashes as path separators - pluginRootPath := filepath.ToSlash(relPath) - return path.Join(pluginRootPath, "module.js") -} - -// convertHashForSRI takes a SHA256 hash string and returns it as expected by the browser for SRI checks. -func convertHashForSRI(h string) string { - hb, err := hex.DecodeString(h) - if err != nil { - return "" - } - return "sha256-" + base64.StdEncoding.EncodeToString(hb) -} - -// cdnEnabled checks if a plugin is loaded via CDN -func cdnEnabled(p *plugins.Plugin, cdn *pluginscdn.Service) bool { - return p.FS.Type().CDN() || cdn.PluginSupported(p.ID) -} diff --git a/pkg/plugins/pluginassets/modulehash_test.go b/pkg/plugins/pluginassets/modulehash_test.go deleted file mode 100644 index c11fcb7f6a9..00000000000 --- a/pkg/plugins/pluginassets/modulehash_test.go +++ /dev/null @@ -1,356 +0,0 @@ -package pluginassets - -import ( - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/config" - "github.com/grafana/grafana/pkg/plugins/pluginscdn" -) - -func TestConvertHashForSRI(t *testing.T) { - for _, tc := range []struct { - hash string - expHash string - expErr bool - }{ - { - hash: "ddfcb449445064e6c39f0c20b15be3cb6a55837cf4781df23d02de005f436811", - expHash: "sha256-3fy0SURQZObDnwwgsVvjy2pVg3z0eB3yPQLeAF9DaBE=", - }, - { - hash: "not-a-valid-hash", - expErr: true, - }, - } { - t.Run(tc.hash, func(t *testing.T) { - r := convertHashForSRI(tc.hash) - if tc.expErr { - // convertHashForSRI returns empty string on error - require.Empty(t, r) - } else { - require.Equal(t, tc.expHash, r) - } - }) - } -} - -func TestCalculateModuleHash(t *testing.T) { - const ( - pluginID = "grafana-test-datasource" - parentPluginID = "grafana-test-app" - ) - - // Helper to create a plugin with manifest - createPluginWithManifest := func(id string, manifest *plugins.PluginManifest, parent *plugins.Plugin) *plugins.Plugin { - p := &plugins.Plugin{ - JSONData: plugins.JSONData{ - ID: id, - }, - Signature: plugins.SignatureStatusValid, - Manifest: manifest, - } - if parent != nil { - p.Parent = parent - } - return p - } - - // Helper to create a v2 manifest - createV2Manifest := func(files map[string]string) *plugins.PluginManifest { - return &plugins.PluginManifest{ - ManifestVersion: "2.0.0", - Files: files, - } - } - - for _, tc := range []struct { - name string - plugin *plugins.Plugin - cfg *config.PluginManagementCfg - cdn *pluginscdn.Service - expModuleHash string - }{ - { - name: "should return empty string when cfg is nil", - plugin: createPluginWithManifest(pluginID, createV2Manifest(map[string]string{ - "module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03", - }), nil), - cfg: nil, - cdn: nil, - expModuleHash: "", - }, - { - name: "should return empty string when SRI checks are disabled", - plugin: createPluginWithManifest(pluginID, createV2Manifest(map[string]string{ - "module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03", - }), nil), - cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: false}}, - cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), - expModuleHash: "", - }, - { - name: "should return empty string for unsigned plugin", - plugin: &plugins.Plugin{ - JSONData: plugins.JSONData{ID: pluginID}, - Signature: plugins.SignatureStatusUnsigned, - Manifest: createV2Manifest(map[string]string{"module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"}), - FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")), - }, - cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, - cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), - expModuleHash: "", - }, - { - name: "should return module hash for valid plugin", - plugin: &plugins.Plugin{ - JSONData: plugins.JSONData{ID: pluginID}, - Signature: plugins.SignatureStatusValid, - Manifest: createV2Manifest(map[string]string{"module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"}), - FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")), - }, - cfg: &config.PluginManagementCfg{ - PluginsCDNURLTemplate: "https://cdn.example.com", - Features: config.Features{SriChecksEnabled: true}, - PluginSettings: config.PluginSettings{ - pluginID: {"cdn": "true"}, - }, - }, - cdn: func() *pluginscdn.Service { - cfg := &config.PluginManagementCfg{ - PluginsCDNURLTemplate: "https://cdn.example.com", - PluginSettings: config.PluginSettings{ - pluginID: {"cdn": "true"}, - }, - } - return pluginscdn.ProvideService(cfg) - }(), - expModuleHash: "sha256-WJG1tSLV3whtD/CxEPvZ0hu0/HFjrzTQgoai6Eb2vgM=", - }, - { - name: "should return empty string when manifest is nil", - plugin: &plugins.Plugin{ - JSONData: plugins.JSONData{ID: pluginID}, - Signature: plugins.SignatureStatusValid, - Manifest: nil, - FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")), - }, - cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, - cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), - expModuleHash: "", - }, - { - name: "should return empty string for v1 manifest", - plugin: &plugins.Plugin{ - JSONData: plugins.JSONData{ID: pluginID}, - Signature: plugins.SignatureStatusValid, - Manifest: &plugins.PluginManifest{ - ManifestVersion: "1.0.0", - Files: map[string]string{"module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"}, - }, - FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")), - }, - cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, - cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), - expModuleHash: "", - }, - { - name: "should return empty string when module.js is not in manifest", - plugin: &plugins.Plugin{ - JSONData: plugins.JSONData{ID: pluginID}, - Signature: plugins.SignatureStatusValid, - Manifest: createV2Manifest(map[string]string{"plugin.json": "129fab4e0584d18c778ebdfa5fe1a68edf2e5c5aeb8290b2c68182c857cb59f8"}), - FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")), - }, - cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, - cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), - expModuleHash: "", - }, - { - name: "missing module.js entry from MANIFEST.txt should not return module hash", - plugin: &plugins.Plugin{ - JSONData: plugins.JSONData{ID: pluginID}, - Signature: plugins.SignatureStatusValid, - Manifest: createV2Manifest(map[string]string{"plugin.json": "129fab4e0584d18c778ebdfa5fe1a68edf2e5c5aeb8290b2c68182c857cb59f8"}), - FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-module-js")), - }, - cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, - cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), - expModuleHash: "", - }, - { - name: "signed status but missing MANIFEST.txt should not return module hash", - plugin: &plugins.Plugin{ - JSONData: plugins.JSONData{ID: pluginID}, - Signature: plugins.SignatureStatusValid, - Manifest: nil, - FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-manifest-txt")), - }, - cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, - cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), - expModuleHash: "", - }, - { - // parentPluginID (/) - // └── pluginID (/datasource) - name: "nested plugin should return module hash from parent MANIFEST.txt", - plugin: func() *plugins.Plugin { - parent := &plugins.Plugin{ - JSONData: plugins.JSONData{ID: parentPluginID}, - Signature: plugins.SignatureStatusValid, - Manifest: createV2Manifest(map[string]string{ - "module.js": "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a", - "datasource/module.js": "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711", - }), - FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested")), - } - return &plugins.Plugin{ - JSONData: plugins.JSONData{ID: pluginID}, - Signature: plugins.SignatureStatusValid, - Parent: parent, - FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "datasource")), - } - }(), - cfg: &config.PluginManagementCfg{ - PluginsCDNURLTemplate: "https://cdn.example.com", - Features: config.Features{SriChecksEnabled: true}, - PluginSettings: config.PluginSettings{ - pluginID: {"cdn": "true"}, - parentPluginID: {"cdn": "true"}, - }, - }, - cdn: func() *pluginscdn.Service { - cfg := &config.PluginManagementCfg{ - PluginsCDNURLTemplate: "https://cdn.example.com", - PluginSettings: config.PluginSettings{ - pluginID: {"cdn": "true"}, - parentPluginID: {"cdn": "true"}, - }, - } - return pluginscdn.ProvideService(cfg) - }(), - expModuleHash: "sha256-BNcNsJHZbEd1+zK6Wo+EzCKJPrQ6/bZJcmZh1EJcZxE=", - }, - { - // parentPluginID (/) - // └── pluginID (/panels/one) - name: "nested plugin deeper than one subfolder should return module hash from parent MANIFEST.txt", - plugin: func() *plugins.Plugin { - parent := &plugins.Plugin{ - JSONData: plugins.JSONData{ID: parentPluginID}, - Signature: plugins.SignatureStatusValid, - Manifest: createV2Manifest(map[string]string{ - "module.js": "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a", - "panels/one/module.js": "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f", - "datasource/module.js": "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711", - }), - FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested")), - } - return &plugins.Plugin{ - JSONData: plugins.JSONData{ID: pluginID}, - Signature: plugins.SignatureStatusValid, - Parent: parent, - FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one")), - } - }(), - cfg: &config.PluginManagementCfg{ - PluginsCDNURLTemplate: "https://cdn.example.com", - Features: config.Features{SriChecksEnabled: true}, - PluginSettings: config.PluginSettings{ - pluginID: {"cdn": "true"}, - parentPluginID: {"cdn": "true"}, - }, - }, - cdn: func() *pluginscdn.Service { - cfg := &config.PluginManagementCfg{ - PluginsCDNURLTemplate: "https://cdn.example.com", - PluginSettings: config.PluginSettings{ - pluginID: {"cdn": "true"}, - parentPluginID: {"cdn": "true"}, - }, - } - return pluginscdn.ProvideService(cfg) - }(), - expModuleHash: "sha256-y9GsIoRkWg4emocipyn1vN0rgxIicocJxjYL7s3WFD8=", - }, - { - // grand-parent-app (/) - // ├── parent-datasource (/datasource) - // │ └── child-panel (/datasource/panels/one) - name: "nested plugin of a nested plugin should return module hash from grandparent MANIFEST.txt", - plugin: func() *plugins.Plugin { - grandparent := &plugins.Plugin{ - JSONData: plugins.JSONData{ID: "grand-parent-app"}, - Signature: plugins.SignatureStatusValid, - Manifest: createV2Manifest(map[string]string{ - "module.js": "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a", - "datasource/module.js": "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711", - "datasource/panels/one/module.js": "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f", - }), - FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested")), - } - parent := &plugins.Plugin{ - JSONData: plugins.JSONData{ID: "parent-datasource"}, - Signature: plugins.SignatureStatusValid, - Parent: grandparent, - FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource")), - } - return &plugins.Plugin{ - JSONData: plugins.JSONData{ID: "child-panel"}, - Signature: plugins.SignatureStatusValid, - Parent: parent, - FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource", "panels", "one")), - } - }(), - cfg: &config.PluginManagementCfg{ - PluginsCDNURLTemplate: "https://cdn.example.com", - Features: config.Features{SriChecksEnabled: true}, - PluginSettings: config.PluginSettings{ - "child-panel": {"cdn": "true"}, - "parent-datasource": {"cdn": "true"}, - "grand-parent-app": {"cdn": "true"}, - }, - }, - cdn: func() *pluginscdn.Service { - cfg := &config.PluginManagementCfg{ - PluginsCDNURLTemplate: "https://cdn.example.com", - PluginSettings: config.PluginSettings{ - "child-panel": {"cdn": "true"}, - "parent-datasource": {"cdn": "true"}, - "grand-parent-app": {"cdn": "true"}, - }, - } - return pluginscdn.ProvideService(cfg) - }(), - expModuleHash: "sha256-y9GsIoRkWg4emocipyn1vN0rgxIicocJxjYL7s3WFD8=", - }, - { - name: "nested plugin should not return module hash when parent manifest is nil", - plugin: func() *plugins.Plugin { - parent := &plugins.Plugin{ - JSONData: plugins.JSONData{ID: parentPluginID}, - Signature: plugins.SignatureStatusValid, - Manifest: nil, // Parent has no manifest - FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested")), - } - return &plugins.Plugin{ - JSONData: plugins.JSONData{ID: pluginID}, - Signature: plugins.SignatureStatusValid, - Parent: parent, - FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one")), - } - }(), - cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}}, - cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}), - expModuleHash: "", - }, - } { - t.Run(tc.name, func(t *testing.T) { - result := CalculateModuleHash(tc.plugin, tc.cfg, tc.cdn) - require.Equal(t, tc.expModuleHash, result) - }) - } -} diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index e1c26b4f87d..bf1b23a35b0 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -40,7 +40,6 @@ type Plugin struct { Pinned bool // Signature fields - Manifest *PluginManifest Signature SignatureStatus SignatureType SignatureType SignatureOrg string @@ -49,9 +48,8 @@ type Plugin struct { Error *Error // SystemJS fields - Module string - ModuleHash string - BaseURL string + Module string + BaseURL string Angular AngularMeta @@ -534,24 +532,3 @@ func (pt Type) IsValid() bool { } return false } - -// PluginManifest holds details for the file manifest -type PluginManifest struct { - Plugin string `json:"plugin"` - Version string `json:"version"` - KeyID string `json:"keyId"` - Time int64 `json:"time"` - Files map[string]string `json:"files"` - - // V2 supported fields - ManifestVersion string `json:"manifestVersion"` - SignatureType SignatureType `json:"signatureType"` - SignedByOrg string `json:"signedByOrg"` - SignedByOrgName string `json:"signedByOrgName"` - RootURLs []string `json:"rootUrls"` -} - -// IsV2 returns true if the manifest is version 2.x -func (m *PluginManifest) IsV2() bool { - return strings.HasPrefix(m.ManifestVersion, "2.") -} diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 5b4f3bed5bb..218e9fabc36 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -376,8 +376,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api keyretrieverService := keyretriever.ProvideService(keyRetriever) signatureSignature := signature.ProvideService(pluginManagementCfg, keyretrieverService) localProvider := pluginassets.NewLocalProvider() - pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg) - bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider, pluginscdnService) + bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider) unsignedPluginAuthorizer := signature.ProvideOSSAuthorizer(pluginManagementCfg) validation := signature.ProvideValidatorService(unsignedPluginAuthorizer) angularpatternsstoreService := angularpatternsstore.ProvideService(kvStore) @@ -715,7 +714,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, pluginstoreService) + pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg) + pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, signatureSignature, pluginstoreService) avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg) prefService := prefimpl.ProvideService(sqlStore, cfg) dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider) @@ -1042,8 +1042,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac keyretrieverService := keyretriever.ProvideService(keyRetriever) signatureSignature := signature.ProvideService(pluginManagementCfg, keyretrieverService) localProvider := pluginassets.NewLocalProvider() - pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg) - bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider, pluginscdnService) + bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider) unsignedPluginAuthorizer := signature.ProvideOSSAuthorizer(pluginManagementCfg) validation := signature.ProvideValidatorService(unsignedPluginAuthorizer) angularpatternsstoreService := angularpatternsstore.ProvideService(kvStore) @@ -1383,7 +1382,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, pluginstoreService) + pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg) + pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, signatureSignature, pluginstoreService) avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg) prefService := prefimpl.ProvideService(sqlStore, cfg) dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider) diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index eee04acedfc..a5a7a7fd8db 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -26,7 +26,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/pluginassets" "github.com/grafana/grafana/pkg/plugins/pluginerrs" - "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins" @@ -214,27 +213,10 @@ func TestLoader_Load(t *testing.T) { ExtensionPoints: []plugins.ExtensionPoint{}, }, }, - Class: plugins.ClassExternal, - Module: "public/plugins/test-app/module.js", - BaseURL: "public/plugins/test-app", - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "includes-symlinks")), - Manifest: &plugins.PluginManifest{ - Plugin: "test-app", - Version: "1.0.0", - KeyID: "7e4d0c6a708866e7", - Time: 1622547655175, - Files: map[string]string{ - "dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303", - "dashboards/extra/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee", - "plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf", - "symlink_to_txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048", - "text.txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048", - }, - ManifestVersion: "2.0.0", - SignatureType: plugins.SignatureTypeGrafana, - SignedByOrg: "grafana", - SignedByOrgName: "Grafana Labs", - }, + Class: plugins.ClassExternal, + Module: "public/plugins/test-app/module.js", + BaseURL: "public/plugins/test-app", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "includes-symlinks")), Signature: "valid", SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -665,24 +647,10 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { Executable: "test", State: plugins.ReleaseStateAlpha, }, - Class: plugins.ClassExternal, - Module: "public/plugins/test-datasource/module.js", - BaseURL: "public/plugins/test-datasource", - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature/plugin")), - Manifest: &plugins.PluginManifest{ - Plugin: "test-datasource", - Version: "1.0.0", - KeyID: "7e4d0c6a708866e7", - Time: 1661171417046, - Files: map[string]string{ - "plugin.json": "203ef4a613c5693c437a665cd67f95e2756a0f71b336b2ffb265db7c180d0b19", - }, - ManifestVersion: "2.0.0", - SignatureType: plugins.SignatureTypePrivate, - SignedByOrg: "willbrowne", - SignedByOrgName: "Will Browne", - RootURLs: []string{"http://localhost:3000/"}, - }, + Class: plugins.ClassExternal, + Module: "public/plugins/test-datasource/module.js", + BaseURL: "public/plugins/test-datasource", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature/plugin")), Signature: "valid", SignatureType: plugins.SignatureTypePrivate, SignatureOrg: "Will Browne", @@ -799,22 +767,8 @@ func TestLoader_Load_RBACReady(t *testing.T) { }, Backend: false, }, - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app-with-roles")), - Class: plugins.ClassExternal, - Manifest: &plugins.PluginManifest{ - Plugin: "test-app", - Version: "1.0.0", - KeyID: "7e4d0c6a708866e7", - Time: 1667484928676, - Files: map[string]string{ - "plugin.json": "3348335ec100392b325f3eeb882a07c729e9cbf0f1ae331239f46840bb1a01eb", - }, - ManifestVersion: "2.0.0", - SignatureType: plugins.SignatureTypePrivate, - SignedByOrg: "gabrielmabille", - SignedByOrgName: "gabrielmabille", - RootURLs: []string{"http://localhost:3000/"}, - }, + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app-with-roles")), + Class: plugins.ClassExternal, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypePrivate, SignatureOrg: "gabrielmabille", @@ -883,22 +837,8 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) { Backend: true, Executable: "test", }, - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature-root-url-uri/plugin")), - Class: plugins.ClassExternal, - Manifest: &plugins.PluginManifest{ - Plugin: "test-datasource", - Version: "1.0.0", - KeyID: "7e4d0c6a708866e7", - Time: 1661171981629, - Files: map[string]string{ - "plugin.json": "203ef4a613c5693c437a665cd67f95e2756a0f71b336b2ffb265db7c180d0b19", - }, - ManifestVersion: "2.0.0", - SignatureType: plugins.SignatureTypePrivate, - SignedByOrg: "willbrowne", - SignedByOrgName: "Will Browne", - RootURLs: []string{"http://localhost:3000/grafana"}, - }, + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature-root-url-uri/plugin")), + Class: plugins.ClassExternal, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypePrivate, SignatureOrg: "Will Browne", @@ -985,24 +925,8 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) { }, Backend: false, }, - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app")), - Class: plugins.ClassExternal, - Manifest: &plugins.PluginManifest{ - Plugin: "test-app", - Version: "1.0.0", - KeyID: "7e4d0c6a708866e7", - Time: 1621356785895, - Files: map[string]string{ - "plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf", - "dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303", - "dashboards/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee", - "dashboards/connections_result.json": "124d85c9c2e40214b83273f764574937a79909cfac3f925276fbb72543c224dc", - }, - ManifestVersion: "2.0.0", - SignatureType: plugins.SignatureTypeGrafana, - SignedByOrg: "grafana", - SignedByOrgName: "Grafana Labs", - }, + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app")), + Class: plugins.ClassExternal, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1093,24 +1017,8 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) { }, Backend: false, }, - FS: mustNewStaticFSForTests(t, pluginDir1), - Class: plugins.ClassExternal, - Manifest: &plugins.PluginManifest{ - Plugin: "test-app", - Version: "1.0.0", - KeyID: "7e4d0c6a708866e7", - Time: 1621356785895, - Files: map[string]string{ - "plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf", - "dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303", - "dashboards/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee", - "dashboards/connections_result.json": "124d85c9c2e40214b83273f764574937a79909cfac3f925276fbb72543c224dc", - }, - ManifestVersion: "2.0.0", - SignatureType: plugins.SignatureTypeGrafana, - SignedByOrg: "grafana", - SignedByOrgName: "Grafana Labs", - }, + FS: mustNewStaticFSForTests(t, pluginDir1), + Class: plugins.ClassExternal, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1272,23 +1180,9 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }, Backend: true, }, - Module: "public/plugins/test-datasource/module.js", - BaseURL: "public/plugins/test-datasource", - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent")), - Manifest: &plugins.PluginManifest{ - Plugin: "test-datasource", - Version: "1.0.0", - KeyID: "7e4d0c6a708866e7", - Time: 1661172777367, - Files: map[string]string{ - "plugin.json": "a029469ace740e9502bfb0d40924d1cccae73d0b18adcd8f1ceb7f17bf36beb8", - "nested/plugin.json": "e64abd35cd211e0e4682974ad5cdd1be7a0b7cd24951d302a16d9e2cb6cefea4", - }, - ManifestVersion: "2.0.0", - SignatureType: plugins.SignatureTypeGrafana, - SignedByOrg: "grafana", - SignedByOrgName: "Grafana Labs", - }, + Module: "public/plugins/test-datasource/module.js", + BaseURL: "public/plugins/test-datasource", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent")), Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1331,23 +1225,9 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { ExtensionPoints: []plugins.ExtensionPoint{}, }, }, - Module: "public/plugins/test-panel/module.js", - BaseURL: "public/plugins/test-panel", - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent/nested")), - Manifest: &plugins.PluginManifest{ - Plugin: "test-datasource", - Version: "1.0.0", - KeyID: "7e4d0c6a708866e7", - Time: 1661172777367, - Files: map[string]string{ - "plugin.json": "a029469ace740e9502bfb0d40924d1cccae73d0b18adcd8f1ceb7f17bf36beb8", - "nested/plugin.json": "e64abd35cd211e0e4682974ad5cdd1be7a0b7cd24951d302a16d9e2cb6cefea4", - }, - ManifestVersion: "2.0.0", - SignatureType: plugins.SignatureTypeGrafana, - SignedByOrg: "grafana", - SignedByOrgName: "Grafana Labs", - }, + Module: "public/plugins/test-panel/module.js", + BaseURL: "public/plugins/test-panel", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent/nested")), Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1495,25 +1375,10 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }, Backend: false, }, - Module: "public/plugins/myorgid-simple-app/module.js", - BaseURL: "public/plugins/myorgid-simple-app", - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist")), - DefaultNavURL: "/plugins/myorgid-simple-app/page/root-page-react", - Manifest: &plugins.PluginManifest{ - Plugin: "myorgid-simple-app", - Version: "%VERSION%", - KeyID: "7e4d0c6a708866e7", - Time: 1642614241713, - Files: map[string]string{ - "plugin.json": "1abecfd0229814f6c284ff3c8dd744548f8d676ab3250cd7902c99dabf11480e", - "child/plugin.json": "66ba0dffaf3b1bfa17eb9a8672918fc66d1001f465b1061f4fc19c2f2c100f51", - }, - ManifestVersion: "2.0.0", - SignatureType: plugins.SignatureTypeGrafana, - SignedByOrg: "grafana", - SignedByOrgName: "Grafana Labs", - RootURLs: []string{}, - }, + Module: "public/plugins/myorgid-simple-app/module.js", + BaseURL: "public/plugins/myorgid-simple-app", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist")), + DefaultNavURL: "/plugins/myorgid-simple-app/page/root-page-react", Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1566,21 +1431,6 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { BaseURL: "public/plugins/myorgid-simple-panel", FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist/child")), IncludedInAppID: parent.ID, - Manifest: &plugins.PluginManifest{ - Plugin: "myorgid-simple-app", - Version: "%VERSION%", - KeyID: "7e4d0c6a708866e7", - Time: 1642614241713, - Files: map[string]string{ - "plugin.json": "1abecfd0229814f6c284ff3c8dd744548f8d676ab3250cd7902c99dabf11480e", - "child/plugin.json": "66ba0dffaf3b1bfa17eb9a8672918fc66d1001f465b1061f4fc19c2f2c100f51", - }, - ManifestVersion: "2.0.0", - SignatureType: plugins.SignatureTypeGrafana, - SignedByOrg: "grafana", - SignedByOrgName: "Grafana Labs", - RootURLs: []string{}, - }, Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, SignatureOrg: "Grafana Labs", @@ -1634,7 +1484,7 @@ func newLoader(t *testing.T, cfg *config.PluginManagementCfg, reg registry.Servi require.NoError(t, err) return ProvideService(cfg, pipeline.ProvideDiscoveryStage(cfg, reg), - pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginAssetsProvider, pluginscdn.ProvideService(cfg)), + pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginAssetsProvider), pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector), pipeline.ProvideInitializationStage(cfg, reg, backendFactory, proc, &pluginfakes.FakeAuthService{}, pluginfakes.NewFakeRoleRegistry(), pluginfakes.NewFakeActionSetRegistry(), pluginfakes.NewFakePluginEnvProvider(), tracing.InitializeTracerForTest(), provisionedplugins.NewNoop()), terminate, errTracker) @@ -1664,7 +1514,7 @@ func newLoaderWithOpts(t *testing.T, cfg *config.PluginManagementCfg, opts loade } return ProvideService(cfg, pipeline.ProvideDiscoveryStage(cfg, reg), - pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginassets.NewLocalProvider(), pluginscdn.ProvideService(cfg)), + pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginassets.NewLocalProvider()), pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector), pipeline.ProvideInitializationStage(cfg, reg, backendFactoryProvider, proc, authServiceRegistry, pluginfakes.NewFakeRoleRegistry(), pluginfakes.NewFakeActionSetRegistry(), pluginfakes.NewFakePluginEnvProvider(), tracing.InitializeTracerForTest(), provisionedplugins.NewNoop()), terminate, errTracker) diff --git a/pkg/services/pluginsintegration/pipeline/pipeline.go b/pkg/services/pluginsintegration/pipeline/pipeline.go index 94b5331ac00..f377e9eb867 100644 --- a/pkg/services/pluginsintegration/pipeline/pipeline.go +++ b/pkg/services/pluginsintegration/pipeline/pipeline.go @@ -18,7 +18,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/pluginassets" - "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/pluginsintegration/coreplugin" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" "github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins" @@ -43,7 +42,7 @@ func ProvideDiscoveryStage(cfg *config.PluginManagementCfg, pr registry.Service) }) } -func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.SignatureCalculator, ap pluginassets.Provider, cdn *pluginscdn.Service) *bootstrap.Bootstrap { +func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.SignatureCalculator, ap pluginassets.Provider) *bootstrap.Bootstrap { disableAlertingForTempoDecorateFunc := func(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { if p.ID == coreplugin.Tempo && !cfg.Features.TempoAlertingEnabled { p.Alerting = false @@ -53,7 +52,7 @@ func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.Signature return bootstrap.New(cfg, bootstrap.Opts{ ConstructFunc: bootstrap.DefaultConstructFunc(cfg, sc, ap), - DecorateFuncs: append(bootstrap.DefaultDecorateFuncs(cfg, cdn), disableAlertingForTempoDecorateFunc), + DecorateFuncs: append(bootstrap.DefaultDecorateFuncs(cfg), disableAlertingForTempoDecorateFunc), }) } diff --git a/pkg/services/pluginsintegration/pluginassets/pluginassets.go b/pkg/services/pluginsintegration/pluginassets/pluginassets.go index 8735f7a4354..4d9a7ec1a53 100644 --- a/pkg/services/pluginsintegration/pluginassets/pluginassets.go +++ b/pkg/services/pluginsintegration/pluginassets/pluginassets.go @@ -2,12 +2,19 @@ package pluginassets import ( "context" + "encoding/base64" + "encoding/hex" + "fmt" + "path" + "path/filepath" + "sync" "github.com/Masterminds/semver/v3" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" ) @@ -21,20 +28,24 @@ var ( scriptLoadingMinSupportedVersion = semver.MustParse(CreatePluginVersionScriptSupportEnabled) ) -func ProvideService(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service, store pluginstore.Store) *Service { +func ProvideService(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service, sig *signature.Signature, store pluginstore.Store) *Service { return &Service{ - cfg: cfg, - cdn: cdn, - store: store, - log: log.New("pluginassets"), + cfg: cfg, + cdn: cdn, + signature: sig, + store: store, + log: log.New("pluginassets"), } } type Service struct { - cfg *config.PluginManagementCfg - cdn *pluginscdn.Service - store pluginstore.Store - log log.Logger + cfg *config.PluginManagementCfg + cdn *pluginscdn.Service + signature *signature.Signature + store pluginstore.Store + log log.Logger + + moduleHashCache sync.Map } // LoadingStrategy calculates the loading strategy for a plugin. @@ -71,6 +82,95 @@ func (s *Service) LoadingStrategy(_ context.Context, p pluginstore.Plugin) plugi return plugins.LoadingStrategyFetch } +// ModuleHash returns the module.js SHA256 hash for a plugin in the format expected by the browser for SRI checks. +// The module hash is read from the plugin's MANIFEST.txt file. +// The plugin can also be a nested plugin. +// If the plugin is unsigned, an empty string is returned. +// The results are cached to avoid repeated reads from the MANIFEST.txt file. +func (s *Service) ModuleHash(ctx context.Context, p pluginstore.Plugin) string { + k := s.moduleHashCacheKey(p) + cachedValue, ok := s.moduleHashCache.Load(k) + if ok { + return cachedValue.(string) + } + mh, err := s.moduleHash(ctx, p, "") + if err != nil { + s.log.Error("Failed to calculate module hash", "plugin", p.ID, "error", err) + } + s.moduleHashCache.Store(k, mh) + return mh +} + +// moduleHash is the underlying function for ModuleHash. See its documentation for more information. +// If the plugin is not a CDN plugin, the function will return an empty string. +// It will read the module hash from the MANIFEST.txt in the [[plugins.FS]] of the provided plugin. +// If childFSBase is provided, the function will try to get the hash from MANIFEST.txt for the provided children's +// module.js file, rather than for the provided plugin. +func (s *Service) moduleHash(ctx context.Context, p pluginstore.Plugin, childFSBase string) (r string, err error) { + if !s.cfg.Features.SriChecksEnabled { + return "", nil + } + + // Ignore unsigned plugins + if !p.Signature.IsValid() { + return "", nil + } + + if p.Parent != nil { + // Nested plugin + parent, ok := s.store.Plugin(ctx, p.Parent.ID) + if !ok { + return "", fmt.Errorf("parent plugin plugin %q for child plugin %q not found", p.Parent.ID, p.ID) + } + + // The module hash is contained within the parent's MANIFEST.txt file. + // For example, the parent's MANIFEST.txt will contain an entry similar to this: + // + // ``` + // "datasource/module.js": "1234567890abcdef..." + // ``` + // + // Recursively call moduleHash with the parent plugin and with the children plugin folder path + // to get the correct module hash for the nested plugin. + if childFSBase == "" { + childFSBase = p.Base() + } + return s.moduleHash(ctx, parent, childFSBase) + } + + // Only CDN plugins are supported for SRI checks. + // CDN plugins have the version as part of the URL, which acts as a cache-buster. + // Needed due to: https://github.com/grafana/plugin-tools/pull/1426 + // FS plugins build before this change will have SRI mismatch issues. + if !s.cdnEnabled(p.ID, p.FS) { + return "", nil + } + + manifest, err := s.signature.ReadPluginManifestFromFS(ctx, p.FS) + if err != nil { + return "", fmt.Errorf("read plugin manifest: %w", err) + } + if !manifest.IsV2() { + return "", nil + } + + var childPath string + if childFSBase != "" { + // Calculate the relative path of the child plugin folder from the parent plugin folder. + childPath, err = p.FS.Rel(childFSBase) + if err != nil { + return "", fmt.Errorf("rel path: %w", err) + } + // MANIFETS.txt uses forward slashes as path separators. + childPath = filepath.ToSlash(childPath) + } + moduleHash, ok := manifest.Files[path.Join(childPath, "module.js")] + if !ok { + return "", nil + } + return convertHashForSRI(moduleHash) +} + func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool { if cpv, ok := ps[CreatePluginVersionCfgKey]; ok { createPluginVer, err := semver.NewVersion(cpv) @@ -88,3 +188,17 @@ func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool { func (s *Service) cdnEnabled(pluginID string, fs plugins.FS) bool { return s.cdn.PluginSupported(pluginID) || fs.Type().CDN() } + +// convertHashForSRI takes a SHA256 hash string and returns it as expected by the browser for SRI checks. +func convertHashForSRI(h string) (string, error) { + hb, err := hex.DecodeString(h) + if err != nil { + return "", fmt.Errorf("hex decode string: %w", err) + } + return "sha256-" + base64.StdEncoding.EncodeToString(hb), nil +} + +// moduleHashCacheKey returns a unique key for the module hash cache. +func (s *Service) moduleHashCacheKey(p pluginstore.Plugin) string { + return p.ID + ":" + p.Info.Version +} diff --git a/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go b/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go index 91b8b515bb1..192717c34ff 100644 --- a/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go +++ b/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go @@ -2,14 +2,19 @@ package pluginassets import ( "context" + "fmt" + "path/filepath" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" + "github.com/grafana/grafana/pkg/plugins/manager/signature" + "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" ) @@ -174,6 +179,349 @@ func TestService_Calculate(t *testing.T) { } } +func TestService_ModuleHash(t *testing.T) { + const ( + pluginID = "grafana-test-datasource" + parentPluginID = "grafana-test-app" + ) + for _, tc := range []struct { + name string + features *config.Features + store []pluginstore.Plugin + + // Can be used to configure plugin's fs + // fs cdn type = loaded from CDN with no files on disk + // fs local type = files on disk but served from CDN only if cdn=true + plugin pluginstore.Plugin + + // When true, set cdn=true in config + cdn bool + expModuleHash string + }{ + { + name: "unsigned should not return module hash", + plugin: newPlugin(pluginID, withSignatureStatus(plugins.SignatureStatusUnsigned)), + cdn: false, + features: &config.Features{SriChecksEnabled: false}, + expModuleHash: "", + }, + { + plugin: newPlugin( + pluginID, + withSignatureStatus(plugins.SignatureStatusValid), + withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), + withClass(plugins.ClassExternal), + ), + cdn: true, + features: &config.Features{SriChecksEnabled: true}, + expModuleHash: newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"), + }, + { + plugin: newPlugin( + pluginID, + withSignatureStatus(plugins.SignatureStatusValid), + withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), + withClass(plugins.ClassExternal), + ), + cdn: true, + features: &config.Features{SriChecksEnabled: true}, + expModuleHash: newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"), + }, + { + plugin: newPlugin( + pluginID, + withSignatureStatus(plugins.SignatureStatusValid), + withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), + ), + cdn: false, + features: &config.Features{SriChecksEnabled: true}, + expModuleHash: "", + }, + { + plugin: newPlugin( + pluginID, + withSignatureStatus(plugins.SignatureStatusValid), + withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), + ), + cdn: true, + features: &config.Features{SriChecksEnabled: false}, + expModuleHash: "", + }, + { + plugin: newPlugin( + pluginID, + withSignatureStatus(plugins.SignatureStatusValid), + withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), + ), + cdn: false, + features: &config.Features{SriChecksEnabled: false}, + expModuleHash: "", + }, + { + // parentPluginID (/) + // └── pluginID (/datasource) + name: "nested plugin should return module hash from parent MANIFEST.txt", + store: []pluginstore.Plugin{ + newPlugin( + parentPluginID, + withSignatureStatus(plugins.SignatureStatusValid), + withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))), + ), + }, + plugin: newPlugin( + pluginID, + withSignatureStatus(plugins.SignatureStatusValid), + withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "datasource"))), + withParent(parentPluginID), + ), + cdn: true, + features: &config.Features{SriChecksEnabled: true}, + expModuleHash: newSRIHash(t, "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711"), + }, + { + // parentPluginID (/) + // └── pluginID (/panels/one) + name: "nested plugin deeper than one subfolder should return module hash from parent MANIFEST.txt", + store: []pluginstore.Plugin{ + newPlugin( + parentPluginID, + withSignatureStatus(plugins.SignatureStatusValid), + withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))), + ), + }, + plugin: newPlugin( + pluginID, + withSignatureStatus(plugins.SignatureStatusValid), + withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one"))), + withParent(parentPluginID), + ), + cdn: true, + features: &config.Features{SriChecksEnabled: true}, + expModuleHash: newSRIHash(t, "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f"), + }, + { + // grand-parent-app (/) + // ├── parent-datasource (/datasource) + // │ └── child-panel (/datasource/panels/one) + name: "nested plugin of a nested plugin should return module hash from parent MANIFEST.txt", + store: []pluginstore.Plugin{ + newPlugin( + "grand-parent-app", + withSignatureStatus(plugins.SignatureStatusValid), + withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested"))), + ), + newPlugin( + "parent-datasource", + withSignatureStatus(plugins.SignatureStatusValid), + withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource"))), + withParent("grand-parent-app"), + ), + }, + plugin: newPlugin( + "child-panel", + withSignatureStatus(plugins.SignatureStatusValid), + withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource", "panels", "one"))), + withParent("parent-datasource"), + ), + cdn: true, + features: &config.Features{SriChecksEnabled: true}, + expModuleHash: newSRIHash(t, "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f"), + }, + { + name: "nested plugin should not return module hash from parent if it's not registered in the store", + store: []pluginstore.Plugin{}, + plugin: newPlugin( + pluginID, + withSignatureStatus(plugins.SignatureStatusValid), + withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one"))), + withParent(parentPluginID), + ), + cdn: false, + features: &config.Features{SriChecksEnabled: true}, + expModuleHash: "", + }, + { + name: "missing module.js entry from MANIFEST.txt should not return module hash", + plugin: newPlugin( + pluginID, + withSignatureStatus(plugins.SignatureStatusValid), + withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-module-js"))), + ), + cdn: false, + features: &config.Features{SriChecksEnabled: true}, + expModuleHash: "", + }, + { + name: "signed status but missing MANIFEST.txt should not return module hash", + plugin: newPlugin( + pluginID, + withSignatureStatus(plugins.SignatureStatusValid), + withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-manifest-txt"))), + ), + cdn: false, + features: &config.Features{SriChecksEnabled: true}, + expModuleHash: "", + }, + } { + if tc.name == "" { + var expS string + if tc.expModuleHash == "" { + expS = "should not return module hash" + } else { + expS = "should return module hash" + } + tc.name = fmt.Sprintf("feature=%v, cdn_config=%v, class=%v %s", tc.features.SriChecksEnabled, tc.cdn, tc.plugin.Class, expS) + } + + t.Run(tc.name, func(t *testing.T) { + var pluginSettings config.PluginSettings + if tc.cdn { + pluginSettings = config.PluginSettings{ + pluginID: { + "cdn": "true", + }, + parentPluginID: map[string]string{ + "cdn": "true", + }, + "grand-parent-app": map[string]string{ + "cdn": "true", + }, + } + } + features := tc.features + if features == nil { + features = &config.Features{} + } + pCfg := &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "http://cdn.example.com", + PluginSettings: pluginSettings, + Features: *features, + } + svc := ProvideService( + pCfg, + pluginscdn.ProvideService(pCfg), + signature.ProvideService(pCfg, statickey.New()), + pluginstore.NewFakePluginStore(tc.store...), + ) + mh := svc.ModuleHash(context.Background(), tc.plugin) + require.Equal(t, tc.expModuleHash, mh) + }) + } +} + +func TestService_ModuleHash_Cache(t *testing.T) { + pCfg := &config.PluginManagementCfg{ + PluginSettings: config.PluginSettings{}, + Features: config.Features{SriChecksEnabled: true}, + } + svc := ProvideService( + pCfg, + pluginscdn.ProvideService(pCfg), + signature.ProvideService(pCfg, statickey.New()), + pluginstore.NewFakePluginStore(), + ) + const pluginID = "grafana-test-datasource" + + t.Run("cache key", func(t *testing.T) { + t.Run("with version", func(t *testing.T) { + const pluginVersion = "1.0.0" + p := newPlugin(pluginID, withInfo(plugins.Info{Version: pluginVersion})) + k := svc.moduleHashCacheKey(p) + require.Equal(t, pluginID+":"+pluginVersion, k, "cache key should be correct") + }) + + t.Run("without version", func(t *testing.T) { + p := newPlugin(pluginID) + k := svc.moduleHashCacheKey(p) + require.Equal(t, pluginID+":", k, "cache key should be correct") + }) + }) + + t.Run("ModuleHash usage", func(t *testing.T) { + pV1 := newPlugin( + pluginID, + withInfo(plugins.Info{Version: "1.0.0"}), + withSignatureStatus(plugins.SignatureStatusValid), + withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), + ) + + pCfg = &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.grafana.com", + PluginSettings: config.PluginSettings{ + pluginID: { + "cdn": "true", + }, + }, + Features: config.Features{SriChecksEnabled: true}, + } + svc = ProvideService( + pCfg, + pluginscdn.ProvideService(pCfg), + signature.ProvideService(pCfg, statickey.New()), + pluginstore.NewFakePluginStore(), + ) + + k := svc.moduleHashCacheKey(pV1) + + _, ok := svc.moduleHashCache.Load(k) + require.False(t, ok, "cache should initially be empty") + + mhV1 := svc.ModuleHash(context.Background(), pV1) + pV1Exp := newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03") + require.Equal(t, pV1Exp, mhV1, "returned value should be correct") + + cachedMh, ok := svc.moduleHashCache.Load(k) + require.True(t, ok) + require.Equal(t, pV1Exp, cachedMh, "cache should contain the returned value") + + t.Run("different version uses different cache key", func(t *testing.T) { + pV2 := newPlugin( + pluginID, + withInfo(plugins.Info{Version: "2.0.0"}), + withSignatureStatus(plugins.SignatureStatusValid), + // different fs for different hash + withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))), + ) + mhV2 := svc.ModuleHash(context.Background(), pV2) + require.NotEqual(t, mhV2, mhV1, "different version should have different hash") + require.Equal(t, newSRIHash(t, "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a"), mhV2) + }) + + t.Run("cache should be used", func(t *testing.T) { + // edit cache directly + svc.moduleHashCache.Store(k, "hax") + require.Equal(t, "hax", svc.ModuleHash(context.Background(), pV1)) + }) + }) +} + +func TestConvertHashFromSRI(t *testing.T) { + for _, tc := range []struct { + hash string + expHash string + expErr bool + }{ + { + hash: "ddfcb449445064e6c39f0c20b15be3cb6a55837cf4781df23d02de005f436811", + expHash: "sha256-3fy0SURQZObDnwwgsVvjy2pVg3z0eB3yPQLeAF9DaBE=", + }, + { + hash: "not-a-valid-hash", + expErr: true, + }, + } { + t.Run(tc.hash, func(t *testing.T) { + r, err := convertHashForSRI(tc.hash) + if tc.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expHash, r) + } + }) + } +} + func newPlugin(pluginID string, cbs ...func(p pluginstore.Plugin) pluginstore.Plugin) pluginstore.Plugin { p := pluginstore.Plugin{ JSONData: plugins.JSONData{ @@ -186,6 +534,13 @@ func newPlugin(pluginID string, cbs ...func(p pluginstore.Plugin) pluginstore.Pl return p } +func withInfo(info plugins.Info) func(p pluginstore.Plugin) pluginstore.Plugin { + return func(p pluginstore.Plugin) pluginstore.Plugin { + p.Info = info + return p + } +} + func withFS(fs plugins.FS) func(p pluginstore.Plugin) pluginstore.Plugin { return func(p pluginstore.Plugin) pluginstore.Plugin { p.FS = fs @@ -193,6 +548,13 @@ func withFS(fs plugins.FS) func(p pluginstore.Plugin) pluginstore.Plugin { } } +func withSignatureStatus(status plugins.SignatureStatus) func(p pluginstore.Plugin) pluginstore.Plugin { + return func(p pluginstore.Plugin) pluginstore.Plugin { + p.Signature = status + return p + } +} + func withAngular(angular bool) func(p pluginstore.Plugin) pluginstore.Plugin { return func(p pluginstore.Plugin) pluginstore.Plugin { p.Angular = plugins.AngularMeta{Detected: angular} @@ -200,6 +562,13 @@ func withAngular(angular bool) func(p pluginstore.Plugin) pluginstore.Plugin { } } +func withParent(parentID string) func(p pluginstore.Plugin) pluginstore.Plugin { + return func(p pluginstore.Plugin) pluginstore.Plugin { + p.Parent = &pluginstore.ParentPlugin{ID: parentID} + return p + } +} + func withClass(class plugins.Class) func(p pluginstore.Plugin) pluginstore.Plugin { return func(p pluginstore.Plugin) pluginstore.Plugin { p.Class = class @@ -218,3 +587,9 @@ func newPluginSettings(pluginID string, kv map[string]string) config.PluginSetti pluginID: kv, } } + +func newSRIHash(t *testing.T, s string) string { + r, err := convertHashForSRI(s) + require.NoError(t, err) + return r +} diff --git a/pkg/plugins/pluginassets/testdata/module-hash-no-manifest-txt/module.js b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-manifest-txt/module.js similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-no-manifest-txt/module.js rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-manifest-txt/module.js diff --git a/pkg/plugins/pluginassets/testdata/module-hash-no-manifest-txt/plugin.json b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-manifest-txt/plugin.json similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-no-manifest-txt/plugin.json rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-manifest-txt/plugin.json diff --git a/pkg/plugins/pluginassets/testdata/module-hash-no-module-js/MANIFEST.txt b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/MANIFEST.txt similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-no-module-js/MANIFEST.txt rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/MANIFEST.txt diff --git a/pkg/plugins/pluginassets/testdata/module-hash-no-module-js/plugin.json b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/plugin.json similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-no-module-js/plugin.json rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/plugin.json diff --git a/pkg/plugins/pluginassets/testdata/module-hash-no-module-js/something.js b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/something.js similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-no-module-js/something.js rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-no-module-js/something.js diff --git a/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/MANIFEST.txt b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/MANIFEST.txt similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/MANIFEST.txt rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/MANIFEST.txt diff --git a/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/module.js b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/module.js similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/module.js rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/module.js diff --git a/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/module.js b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/module.js similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/module.js rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/module.js diff --git a/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/plugin.json b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/plugin.json similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/plugin.json rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/panels/one/plugin.json diff --git a/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/plugin.json b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/plugin.json similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/plugin.json rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/datasource/plugin.json diff --git a/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/module.js b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/module.js similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/module.js rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/module.js diff --git a/pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/plugin.json b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/plugin.json similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-valid-deeply-nested/plugin.json rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-deeply-nested/plugin.json diff --git a/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/MANIFEST.txt b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/MANIFEST.txt similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-valid-nested/MANIFEST.txt rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/MANIFEST.txt diff --git a/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/datasource/module.js b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/datasource/module.js similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-valid-nested/datasource/module.js rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/datasource/module.js diff --git a/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/datasource/plugin.json b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/datasource/plugin.json similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-valid-nested/datasource/plugin.json rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/datasource/plugin.json diff --git a/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/module.js b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/module.js similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-valid-nested/module.js rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/module.js diff --git a/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/panels/one/module.js b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/panels/one/module.js similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-valid-nested/panels/one/module.js rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/panels/one/module.js diff --git a/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/panels/one/plugin.json b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/panels/one/plugin.json similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-valid-nested/panels/one/plugin.json rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/panels/one/plugin.json diff --git a/pkg/plugins/pluginassets/testdata/module-hash-valid-nested/plugin.json b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/plugin.json similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-valid-nested/plugin.json rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid-nested/plugin.json diff --git a/pkg/plugins/pluginassets/testdata/module-hash-valid/MANIFEST.txt b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/MANIFEST.txt similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-valid/MANIFEST.txt rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/MANIFEST.txt diff --git a/pkg/plugins/pluginassets/testdata/module-hash-valid/module.js b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/module.js similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-valid/module.js rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/module.js diff --git a/pkg/plugins/pluginassets/testdata/module-hash-valid/plugin.json b/pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/plugin.json similarity index 100% rename from pkg/plugins/pluginassets/testdata/module-hash-valid/plugin.json rename to pkg/services/pluginsintegration/pluginassets/testdata/module-hash-valid/plugin.json diff --git a/pkg/services/pluginsintegration/pluginstore/plugins.go b/pkg/services/pluginsintegration/pluginstore/plugins.go index 77fe1365fc3..f4504d254c5 100644 --- a/pkg/services/pluginsintegration/pluginstore/plugins.go +++ b/pkg/services/pluginsintegration/pluginstore/plugins.go @@ -30,9 +30,8 @@ type Plugin struct { Error *plugins.Error // SystemJS fields - Module string - BaseURL string - ModuleHash string + Module string + BaseURL string Angular plugins.AngularMeta @@ -81,7 +80,6 @@ func ToGrafanaDTO(p *plugins.Plugin) Plugin { ExternalService: p.ExternalService, Angular: p.Angular, Translations: p.Translations, - ModuleHash: p.ModuleHash, } if p.Parent != nil { diff --git a/pkg/services/pluginsintegration/test_helper.go b/pkg/services/pluginsintegration/test_helper.go index 9957fc11b22..9daad43e3e2 100644 --- a/pkg/services/pluginsintegration/test_helper.go +++ b/pkg/services/pluginsintegration/test_helper.go @@ -24,7 +24,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/pluginassets" "github.com/grafana/grafana/pkg/plugins/pluginerrs" - "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/coreplugin" "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" @@ -50,7 +49,7 @@ func CreateIntegrationTestCtx(t *testing.T, cfg *setting.Cfg, coreRegistry *core proc := process.ProvideService() disc := pipeline.ProvideDiscoveryStage(pCfg, reg) - boot := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), pluginassets.NewLocalProvider(), pluginscdn.ProvideService(pCfg)) + boot := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), pluginassets.NewLocalProvider()) valid := pipeline.ProvideValidationStage(pCfg, signature.NewValidator(signature.NewUnsignedAuthorizer(pCfg)), angularInspector) init := pipeline.ProvideInitializationStage(pCfg, reg, coreplugin.ProvideCoreProvider(coreRegistry), proc, &pluginfakes.FakeAuthService{}, pluginfakes.NewFakeRoleRegistry(), pluginfakes.NewFakeActionSetRegistry(), nil, tracing.InitializeTracerForTest(), provisionedplugins.NewNoop()) term, err := pipeline.ProvideTerminationStage(pCfg, reg, proc) @@ -88,7 +87,7 @@ func CreateTestLoader(t *testing.T, cfg *pluginsCfg.PluginManagementCfg, opts Lo } if opts.Bootstrapper == nil { - opts.Bootstrapper = pipeline.ProvideBootstrapStage(cfg, signature.ProvideService(cfg, statickey.New()), pluginassets.NewLocalProvider(), pluginscdn.ProvideService(cfg)) + opts.Bootstrapper = pipeline.ProvideBootstrapStage(cfg, signature.ProvideService(cfg, statickey.New()), pluginassets.NewLocalProvider()) } if opts.Validator == nil { From 60abd9a159139702769f0069a6de5ac3bd30f524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ida=20=C5=A0tambuk?= Date: Tue, 13 Jan 2026 19:42:47 +0100 Subject: [PATCH 13/19] Dynamic dashboards: Add tests for custom grid repeats (#114545) --- .github/CODEOWNERS | 1 + .../dashboards-repeats-custom-grid.spec.ts | 150 +++++- e2e-playwright/dashboard-new-layouts/utils.ts | 9 + .../dashboards/V2DashWithRowRepeats.json | 486 ++++++++++++++++++ 4 files changed, 640 insertions(+), 6 deletions(-) create mode 100644 e2e-playwright/dashboards/V2DashWithRowRepeats.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 98eb0aee15a..ca9de90ba02 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -440,6 +440,7 @@ i18next.config.ts @grafana/grafana-frontend-platform /e2e-playwright/dashboards/TestDashboard.json @grafana/dashboards-squad @grafana/grafana-search-navigate-organise /e2e-playwright/dashboards/TestV2Dashboard.json @grafana/dashboards-squad /e2e-playwright/dashboards/V2DashWithRepeats.json @grafana/dashboards-squad +/e2e-playwright/dashboards/V2DashWithRowRepeats.json @grafana/dashboards-squad /e2e-playwright/dashboards/V2DashWithTabRepeats.json @grafana/dashboards-squad /e2e-playwright/dashboards-suite/adhoc-filter-from-panel.spec.ts @grafana/datapro /e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts @grafana/grafana-search-navigate-organise diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts index 8cc1f552377..e60b722c46e 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts @@ -1,6 +1,7 @@ import { test, expect } from '@grafana/plugin-e2e'; import testV2DashWithRepeats from '../dashboards/V2DashWithRepeats.json'; +import testV2DashWithRowRepeats from '../dashboards/V2DashWithRowRepeats.json'; import { checkRepeatedPanelTitles, @@ -10,11 +11,14 @@ import { saveDashboard, importTestDashboard, goToEmbeddedPanel, + goToPanelSnapshot, } from './utils'; const repeatTitleBase = 'repeat - '; const newTitleBase = 'edited rep - '; const repeatOptions = [1, 2, 3, 4]; +const getTitleInRepeatRow = (rowIndex: number, panelIndex: number) => + `repeated-row-${rowIndex}-repeated-panel-${panelIndex}`; test.use({ featureToggles: { @@ -165,9 +169,7 @@ test.describe( ) ).toBeVisible(); - await dashboardPage - .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) - .click(); + await page.keyboard.press('Escape'); await expect( dashboardPage.getByGrafanaSelector(selectors.components.DashboardEditPaneSplitter.primaryBody) @@ -217,9 +219,7 @@ test.describe( ) ).toBeVisible(); - await dashboardPage - .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) - .click(); + await page.keyboard.press('Escape'); await expect( dashboardPage.getByGrafanaSelector(selectors.components.DashboardEditPaneSplitter.primaryBody) @@ -405,5 +405,143 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer).all() ).toHaveLength(3); }); + + test('can view repeated panel in a repeated row', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Custom grid repeats - view repeated panel in a repeated row', + JSON.stringify(testV2DashWithRowRepeats) + ); + + // make sure the repeated panel is present in multiple rows + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1))) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(2, 2))) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1))) + .hover(); + + await page.keyboard.press('v'); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(2, 2))) + ).not.toBeVisible(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1))) + ).toBeVisible(); + + const repeatedPanelUrl = page.url(); + + await page.keyboard.press('Escape'); + + // load view panel directly + await page.goto(repeatedPanelUrl); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1))) + ).toBeVisible(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(2, 2))) + ).not.toBeVisible(); + }); + + test('can view embedded panel in a repeated row', async ({ dashboardPage, selectors, page }) => { + const embedPanelTitle = 'embedded-panel'; + await importTestDashboard( + page, + selectors, + 'Custom grid repeats - view embedded repeated panel in a repeated row', + JSON.stringify(testV2DashWithRowRepeats) + ); + + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1))) + .hover(); + await page.keyboard.press('p+e'); + + await goToEmbeddedPanel(page); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1))) + ).toBeVisible(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(2, 2))) + ).not.toBeVisible(); + }); + + // there is a bug in the Snapshot feature that prevents the next two tests from passing + // tracking issue: https://github.com/grafana/grafana/issues/114509 + test.skip('can view repeated panel inside snapshot', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Custom grid repeats - view repeated panel inside snapshot', + JSON.stringify(testV2DashWithRowRepeats) + ); + + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1))) + .hover(); + await page.keyboard.press('p+s'); + + // click "Publish snapshot" + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareSnapshot.publishSnapshot) + .click(); + + // click "Copy link" button in the snapshot drawer + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareSnapshot.copyUrlButton) + .click(); + + await goToPanelSnapshot(page); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1))) + ).toBeVisible(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(2, 2))) + ).not.toBeVisible(); + }); + test.skip('can view single panel in a repeated row inside snapshot', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Custom grid repeats - view single panel inside snapshot', + JSON.stringify(testV2DashWithRowRepeats) + ); + + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('single panel row 1')).hover(); + // open panel snapshot + await page.keyboard.press('p+s'); + + // click "Publish snapshot" + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareSnapshot.publishSnapshot) + .click(); + + // click "Copy link" button + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareSnapshot.copyUrlButton) + .click(); + + await goToPanelSnapshot(page); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('single panel row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1))) + ).toBeHidden(); + }); } ); diff --git a/e2e-playwright/dashboard-new-layouts/utils.ts b/e2e-playwright/dashboard-new-layouts/utils.ts index ade6825b7c1..d2f196f1466 100644 --- a/e2e-playwright/dashboard-new-layouts/utils.ts +++ b/e2e-playwright/dashboard-new-layouts/utils.ts @@ -218,6 +218,15 @@ export async function goToEmbeddedPanel(page: Page) { await page.goto(soloPanelUrl!); } +export async function goToPanelSnapshot(page: Page) { + // extracting snapshot url from clipboard + const snapshotUrl = await page.evaluate(() => navigator.clipboard.readText()); + + expect(snapshotUrl).toBeDefined(); + + await page.goto(snapshotUrl); +} + export async function moveTab( dashboardPage: DashboardPage, page: Page, diff --git a/e2e-playwright/dashboards/V2DashWithRowRepeats.json b/e2e-playwright/dashboards/V2DashWithRowRepeats.json new file mode 100644 index 00000000000..2438908b823 --- /dev/null +++ b/e2e-playwright/dashboards/V2DashWithRowRepeats.json @@ -0,0 +1,486 @@ +{ + "apiVersion": "dashboard.grafana.app/v2beta1", + "kind": "Dashboard", + "metadata": { + "name": "ad8l8fz", + "namespace": "default", + "uid": "fLb2na54K8NZHvn8LfWGL1jhZh03Hy0xpV1KzMYgAXEX", + "resourceVersion": "1", + "generation": 2, + "creationTimestamp": "2025-11-25T15:52:42Z", + "labels": { + "grafana.app/deprecatedInternalID": "20" + }, + "annotations": { + "grafana.app/createdBy": "user:aerwo725ot62od", + "grafana.app/updatedBy": "user:aerwo725ot62od", + "grafana.app/updatedTimestamp": "2025-11-25T15:52:42Z", + "grafana.app/folder": "" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": true, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "query": { + "datasource": { + "name": "-- Grafana --" + }, + "group": "grafana", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + } + } + } + ], + "cursorSync": "Off", + "description": "", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "group": "", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 4, + "links": [], + "title": "repeated-row-$c4-repeated-panel-$c3", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "12.4.0-pre" + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "group": "", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 2, + "links": [], + "title": "single panel row $c4", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "12.4.0-pre" + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + }, + "height": 10, + "repeat": { + "direction": "h", + "mode": "variable", + "value": "c3" + }, + "width": 24, + "x": 0, + "y": 0 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + }, + "height": 8, + "width": 12, + "x": 0, + "y": 10 + } + } + ] + } + }, + "repeat": { + "mode": "variable", + "value": "c4" + }, + "title": "Repeated row $c4" + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "autoRefresh": "", + "autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "fiscalYearStartMonth": 0, + "from": "now-6h", + "hideTimepicker": false, + "timezone": "browser", + "to": "now" + }, + "title": "test-e2e-repeats", + "variables": [ + { + "kind": "CustomVariable", + "spec": { + "allowCustomValue": true, + "current": { + "text": ["1", "2", "3", "4"], + "value": ["1", "2", "3", "4"] + }, + "hide": "dontHide", + "includeAll": true, + "multi": true, + "name": "c1", + "options": [ + { + "selected": true, + "text": "1", + "value": "1" + }, + { + "selected": true, + "text": "2", + "value": "2" + }, + { + "selected": true, + "text": "3", + "value": "3" + }, + { + "selected": true, + "text": "4", + "value": "4" + } + ], + "query": "1,2,3,4", + "skipUrlSync": false + } + }, + { + "kind": "CustomVariable", + "spec": { + "allowCustomValue": true, + "current": { + "text": ["A", "B", "C", "D"], + "value": ["A", "B", "C", "D"] + }, + "hide": "dontHide", + "includeAll": true, + "multi": true, + "name": "c2", + "options": [ + { + "selected": true, + "text": "A", + "value": "A" + }, + { + "selected": true, + "text": "B", + "value": "B" + }, + { + "selected": true, + "text": "C", + "value": "C" + }, + { + "selected": true, + "text": "D", + "value": "D" + } + ], + "query": "A,B,C,D", + "skipUrlSync": false + } + }, + { + "kind": "CustomVariable", + "spec": { + "allowCustomValue": true, + "current": { + "text": ["1", "2", "3", "4"], + "value": ["1", "2", "3", "4"] + }, + "hide": "dontHide", + "includeAll": false, + "multi": true, + "name": "c3", + "options": [ + { + "selected": true, + "text": "1", + "value": "1" + }, + { + "selected": true, + "text": "2", + "value": "2" + }, + { + "selected": true, + "text": "3", + "value": "3" + }, + { + "selected": true, + "text": "4", + "value": "4" + } + ], + "query": "1, 2, 3, 4", + "skipUrlSync": false + } + }, + { + "kind": "CustomVariable", + "spec": { + "allowCustomValue": true, + "current": { + "text": ["1", "2", "3", "4"], + "value": ["1", "2", "3", "4"] + }, + "hide": "dontHide", + "includeAll": false, + "multi": true, + "name": "c4", + "options": [ + { + "selected": true, + "text": "1", + "value": "1" + }, + { + "selected": true, + "text": "2", + "value": "2" + }, + { + "selected": true, + "text": "3", + "value": "3" + }, + { + "selected": true, + "text": "4", + "value": "4" + } + ], + "query": "1, 2, 3, 4", + "skipUrlSync": false + } + } + ] + }, + "status": {} +} From 82d8d44977d8149d8a8e4f2c30ec50b2b8a633e5 Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:44:36 -0700 Subject: [PATCH 14/19] Dashboard Conversion: Remove duplicated data loss function (#116214) remove duplicated dataloss function --- .../conversion/v2alpha1_to_v1beta1.go | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go index 18d9ae90814..857af7ca866 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go @@ -71,11 +71,6 @@ func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec) (m if err != nil { return nil, fmt.Errorf("failed to convert panels: %w", err) } - // Count total panels including those in collapsed rows - totalPanelsConverted := countTotalPanels(panels) - if totalPanelsConverted < len(in.Elements) { - return nil, fmt.Errorf("some panels were not converted from v2alpha1 to v1beta1") - } if len(panels) > 0 { dashboard["panels"] = panels @@ -198,29 +193,6 @@ func convertLinksToV1(links []dashv2alpha1.DashboardDashboardLink) []map[string] return result } -// countTotalPanels counts all panels including those nested in collapsed row panels. -func countTotalPanels(panels []interface{}) int { - count := 0 - for _, p := range panels { - panel, ok := p.(map[string]interface{}) - if !ok { - count++ - continue - } - - // Check if this is a row panel with nested panels - if panelType, ok := panel["type"].(string); ok && panelType == "row" { - if nestedPanels, ok := panel["panels"].([]interface{}); ok { - count += len(nestedPanels) - } - // Don't count the row itself as a panel element - } else { - count++ - } - } - return count -} - // convertPanelsFromElementsAndLayout converts V2 layout structures to V1 panel arrays. // V1 only supports a flat array of panels with row panels for grouping. // This function dispatches to the appropriate converter based on layout type: From 6db51cbdb94ec76f863c54c514dc3a234f3bdc0d Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Tue, 13 Jan 2026 14:54:42 -0500 Subject: [PATCH 15/19] Legends: Revert scrolled truncated legend for now (#116217) * Revert "PieChart: Fix right-oriented legends (#116084)" This reverts commit 0c8c886930f31f805b4ee2f85e082cc0f5b5014e. * Revert "TimeSeries: Fix truncated label text in legend table mode (#115647)" This reverts commit f91efcfe2c8f6e6078d64118e3fd1332f9c12873. --- .../panel_test_piechart.v42.json | 26 +--- .../panel-piechart/panel_test_piechart.json | 26 +--- .../VizLegend/VizLegendTable.test.tsx | 78 ------------ .../VizLegend/VizLegendTableItem.test.tsx | 112 ------------------ .../VizLegend/VizLegendTableItem.tsx | 65 ++++------ 5 files changed, 30 insertions(+), 277 deletions(-) delete mode 100644 packages/grafana-ui/src/components/VizLegend/VizLegendTable.test.tsx delete mode 100644 packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.test.tsx diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-piechart/panel_test_piechart.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-piechart/panel_test_piechart.v42.json index 5b07f246ae3..f705124be5b 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-piechart/panel_test_piechart.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-piechart/panel_test_piechart.v42.json @@ -290,7 +290,7 @@ ], "legend": { "displayMode": "table", - "placement": "right", + "placement": "bottom", "showLegend": true, "values": [ "percent" @@ -304,7 +304,7 @@ "fields": "", "values": false }, - "showLegend": true, + "showLegend": false, "strokeWidth": 1, "text": {} }, @@ -323,15 +323,6 @@ } ], "title": "Percent", - "transformations": [ - { - "id": "renameByRegex", - "options": { - "regex": "^Backend-(.*)$", - "renamePattern": "b-$1" - } - } - ], "type": "piechart" }, { @@ -375,7 +366,7 @@ ], "legend": { "displayMode": "table", - "placement": "right", + "placement": "bottom", "showLegend": true, "values": [ "value" @@ -389,7 +380,7 @@ "fields": "", "values": false }, - "showLegend": true, + "showLegend": false, "strokeWidth": 1, "text": {} }, @@ -408,15 +399,6 @@ } ], "title": "Value", - "transformations": [ - { - "id": "renameByRegex", - "options": { - "regex": "(.*)", - "renamePattern": "$1-how-much-wood-could-a-woodchuck-chuck-if-a-woodchuck-could-chuck-wood" - } - } - ], "type": "piechart" }, { diff --git a/devenv/dev-dashboards/panel-piechart/panel_test_piechart.json b/devenv/dev-dashboards/panel-piechart/panel_test_piechart.json index ac11fd803b9..4333993ea8e 100644 --- a/devenv/dev-dashboards/panel-piechart/panel_test_piechart.json +++ b/devenv/dev-dashboards/panel-piechart/panel_test_piechart.json @@ -248,7 +248,7 @@ "legend": { "values": ["percent"], "displayMode": "table", - "placement": "right" + "placement": "bottom" }, "pieType": "pie", "reduceOptions": { @@ -256,7 +256,7 @@ "fields": "", "values": false }, - "showLegend": true, + "showLegend": false, "strokeWidth": 1, "text": {} }, @@ -272,15 +272,6 @@ "timeFrom": null, "timeShift": null, "title": "Percent", - "transformations": [ - { - "id": "renameByRegex", - "options": { - "regex": "^Backend-(.*)$", - "renamePattern": "b-$1" - } - } - ], "type": "piechart" }, { @@ -320,7 +311,7 @@ "legend": { "values": ["value"], "displayMode": "table", - "placement": "right" + "placement": "bottom" }, "pieType": "pie", "reduceOptions": { @@ -328,7 +319,7 @@ "fields": "", "values": false }, - "showLegend": true, + "showLegend": false, "strokeWidth": 1, "text": {} }, @@ -344,15 +335,6 @@ "timeFrom": null, "timeShift": null, "title": "Value", - "transformations": [ - { - "id": "renameByRegex", - "options": { - "regex": "(.*)", - "renamePattern": "$1-how-much-wood-could-a-woodchuck-chuck-if-a-woodchuck-could-chuck-wood" - } - } - ], "type": "piechart" }, { diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTable.test.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTable.test.tsx deleted file mode 100644 index 131133bcdfb..00000000000 --- a/packages/grafana-ui/src/components/VizLegend/VizLegendTable.test.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { render, screen } from '@testing-library/react'; - -import { VizLegendTable } from './VizLegendTable'; -import { VizLegendItem } from './types'; - -describe('VizLegendTable', () => { - const mockItems: VizLegendItem[] = [ - { label: 'Series 1', color: 'red', yAxis: 1 }, - { label: 'Series 2', color: 'blue', yAxis: 1 }, - { label: 'Series 3', color: 'green', yAxis: 1 }, - ]; - - it('renders without crashing', () => { - const { container } = render(); - expect(container.querySelector('table')).toBeInTheDocument(); - }); - - it('renders all items', () => { - render(); - expect(screen.getByText('Series 1')).toBeInTheDocument(); - expect(screen.getByText('Series 2')).toBeInTheDocument(); - expect(screen.getByText('Series 3')).toBeInTheDocument(); - }); - - it('renders table headers when items have display values', () => { - const itemsWithStats: VizLegendItem[] = [ - { - label: 'Series 1', - color: 'red', - yAxis: 1, - getDisplayValues: () => [ - { numeric: 100, text: '100', title: 'Max' }, - { numeric: 50, text: '50', title: 'Min' }, - ], - }, - ]; - render(); - expect(screen.getByText('Max')).toBeInTheDocument(); - expect(screen.getByText('Min')).toBeInTheDocument(); - }); - - it('renders sort icon when sorted', () => { - const { container } = render( - - ); - expect(container.querySelector('svg')).toBeInTheDocument(); - }); - - it('calls onToggleSort when header is clicked', () => { - const onToggleSort = jest.fn(); - render(); - const header = screen.getByText('Name'); - header.click(); - expect(onToggleSort).toHaveBeenCalledWith('Name'); - }); - - it('does not call onToggleSort when not sortable', () => { - const onToggleSort = jest.fn(); - render(); - const header = screen.getByText('Name'); - header.click(); - expect(onToggleSort).not.toHaveBeenCalled(); - }); - - it('renders with long labels', () => { - const itemsWithLongLabels: VizLegendItem[] = [ - { - label: 'This is a very long series name that should be scrollable within its table cell', - color: 'red', - yAxis: 1, - }, - ]; - render(); - expect( - screen.getByText('This is a very long series name that should be scrollable within its table cell') - ).toBeInTheDocument(); - }); -}); diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.test.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.test.tsx deleted file mode 100644 index 4ca95aa395c..00000000000 --- a/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.test.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { render, screen } from '@testing-library/react'; - -import { LegendTableItem } from './VizLegendTableItem'; -import { VizLegendItem } from './types'; - -describe('LegendTableItem', () => { - const mockItem: VizLegendItem = { - label: 'Series 1', - color: 'red', - yAxis: 1, - }; - - it('renders without crashing', () => { - const { container } = render( - - - - -
- ); - expect(container.querySelector('tr')).toBeInTheDocument(); - }); - - it('renders label text', () => { - render( - - - - -
- ); - expect(screen.getByText('Series 1')).toBeInTheDocument(); - }); - - it('renders with long label text', () => { - const longLabelItem: VizLegendItem = { - ...mockItem, - label: 'This is a very long series name that should be scrollable in the table cell', - }; - render( - - - - -
- ); - expect( - screen.getByText('This is a very long series name that should be scrollable in the table cell') - ).toBeInTheDocument(); - }); - - it('renders stat values when provided', () => { - const itemWithStats: VizLegendItem = { - ...mockItem, - getDisplayValues: () => [ - { numeric: 100, text: '100', title: 'Max' }, - { numeric: 50, text: '50', title: 'Min' }, - ], - }; - render( - - - - -
- ); - expect(screen.getByText('100')).toBeInTheDocument(); - expect(screen.getByText('50')).toBeInTheDocument(); - }); - - it('renders right y-axis indicator when yAxis is 2', () => { - const rightAxisItem: VizLegendItem = { - ...mockItem, - yAxis: 2, - }; - render( - - - - -
- ); - expect(screen.getByText('(right y-axis)')).toBeInTheDocument(); - }); - - it('calls onLabelClick when label is clicked', () => { - const onLabelClick = jest.fn(); - render( - - - - -
- ); - const button = screen.getByRole('button'); - button.click(); - expect(onLabelClick).toHaveBeenCalledWith(mockItem, expect.any(Object)); - }); - - it('does not call onClick when readonly', () => { - const onLabelClick = jest.fn(); - render( - - - - -
- ); - const button = screen.getByRole('button'); - expect(button).toBeDisabled(); - }); -}); diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx index 6de77fd660b..335cf4309e9 100644 --- a/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx +++ b/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx @@ -69,7 +69,7 @@ export const LegendTableItem = ({ return ( - + -
- -
+
{item.getDisplayValues && @@ -130,28 +128,6 @@ const getStyles = (theme: GrafanaTheme2) => { background: rowHoverBg, }, }), - labelCell: css({ - label: 'LegendLabelCell', - maxWidth: 0, - width: '100%', - minWidth: theme.spacing(16), - }), - labelCellInner: css({ - label: 'LegendLabelCellInner', - display: 'block', - flex: 1, - minWidth: 0, - overflowX: 'auto', - overflowY: 'hidden', - paddingRight: theme.spacing(3), - scrollbarWidth: 'none', - msOverflowStyle: 'none', - maskImage: `linear-gradient(to right, black calc(100% - ${theme.spacing(3)}), transparent 100%)`, - WebkitMaskImage: `linear-gradient(to right, black calc(100% - ${theme.spacing(3)}), transparent 100%)`, - '&::-webkit-scrollbar': { - display: 'none', - }, - }), label: css({ label: 'LegendLabel', whiteSpace: 'nowrap', @@ -159,6 +135,9 @@ const getStyles = (theme: GrafanaTheme2) => { border: 'none', fontSize: 'inherit', padding: 0, + maxWidth: '600px', + textOverflow: 'ellipsis', + overflow: 'hidden', userSelect: 'text', }), labelDisabled: css({ From e2f2011d9e66415e80951af4bcf3be38b3c464fb Mon Sep 17 00:00:00 2001 From: Anton Chimrov <89776717+chim678@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:01:22 +0100 Subject: [PATCH 16/19] Restore Canvas element key simplification to prevent blinking icons (#113693) * Simplify Canvas element key to prevent blinking icons * Fix formatting with prettier --- public/app/features/canvas/runtime/element.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/public/app/features/canvas/runtime/element.tsx b/public/app/features/canvas/runtime/element.tsx index 4a7c5ec8a7b..c1e3d519d8f 100644 --- a/public/app/features/canvas/runtime/element.tsx +++ b/public/app/features/canvas/runtime/element.tsx @@ -1108,12 +1108,7 @@ export class ElementState implements LayerElement { tabIndex={0} style={{ userSelect: 'none' }} > - + {this.showActionConfirmation && this.renderActionsConfirmModal(this.getPrimaryAction())} {this.showActionVarsModal && this.renderVariablesInputModal(this.getPrimaryAction())} From d3beed7dd23aa0ccd68e1aaf01728569491da774 Mon Sep 17 00:00:00 2001 From: sabithamuppuri Date: Tue, 13 Jan 2026 13:37:09 -0800 Subject: [PATCH 17/19] Docs: add unified_alerting.state_history configuration section (fixes #114670) (#115607) Co-authored-by: Pepe Cano <825430+ppcano@users.noreply.github.com> Co-authored-by: Johnny Kartheiser <140559259+JohnnyK-Grafana@users.noreply.github.com> --- .../setup-grafana/configure-grafana/_index.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index d9125991c12..d9c5f524bd3 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -2030,6 +2030,44 @@ For example: `disabled_labels=grafana_folder`
+### `[unified_alerting.state_history]` + +This section configures where Grafana Alerting writes alert state history. Refer to [Configure alert state history](/docs/grafana//alerting/set-up/configure-alert-state-history/) for end-to-end setup and examples. + +#### `enabled ` + +Enables recording alert state history. Default is `false`. + +#### `backend ` + +Select the backend used to store alert state history. Supported values: `loki`, `prometheus`, `multiple`. + +#### `loki_remote_url ` + +The URL of the Loki server used when `backend = loki` (or when `backend = multiple` and Loki is a primary/secondary). + +#### `prometheus_target_datasource_uid ` + +Target Prometheus data source UID used for writing alert state changes when `backend = prometheus` (or when `backend = multiple` and Prometheus is a secondary). + +#### `prometheus_metric_name ` + +Optional. Metric name for the alert state metric. Default is `GRAFANA_ALERTS`. + +#### `prometheus_write_timeout ` + +Optional. Timeout for writing alert state data to the target data source. Default is `10s`. + +#### `primary ` + +Used only when `backend = multiple`. Selects the primary backend (for example `loki`). + +#### `secondaries ` + +Used only when `backend = multiple`. Comma-separated list of secondary backends (for example `prometheus`). + +
+ ### `[unified_alerting.state_history.annotations]` This section controls retention of annotations automatically created while evaluating alert rules when alerting state history backend is configured to be annotations (see setting [unified_alerting.state_history].backend) From 215d25ef69d2f71363811f631d472dcc08053a19 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 00:43:07 +0000 Subject: [PATCH 18/19] I18n: Download translations from Crowdin (#116232) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 47 +++++++++++++++++++++++++++++ public/locales/de-DE/grafana.json | 47 +++++++++++++++++++++++++++++ public/locales/es-ES/grafana.json | 47 +++++++++++++++++++++++++++++ public/locales/fr-FR/grafana.json | 47 +++++++++++++++++++++++++++++ public/locales/hu-HU/grafana.json | 47 +++++++++++++++++++++++++++++ public/locales/id-ID/grafana.json | 47 +++++++++++++++++++++++++++++ public/locales/it-IT/grafana.json | 47 +++++++++++++++++++++++++++++ public/locales/ja-JP/grafana.json | 47 +++++++++++++++++++++++++++++ public/locales/ko-KR/grafana.json | 47 +++++++++++++++++++++++++++++ public/locales/nl-NL/grafana.json | 47 +++++++++++++++++++++++++++++ public/locales/pl-PL/grafana.json | 47 +++++++++++++++++++++++++++++ public/locales/pt-BR/grafana.json | 47 +++++++++++++++++++++++++++++ public/locales/pt-PT/grafana.json | 47 +++++++++++++++++++++++++++++ public/locales/ru-RU/grafana.json | 47 +++++++++++++++++++++++++++++ public/locales/sv-SE/grafana.json | 47 +++++++++++++++++++++++++++++ public/locales/tr-TR/grafana.json | 47 +++++++++++++++++++++++++++++ public/locales/zh-Hans/grafana.json | 47 +++++++++++++++++++++++++++++ public/locales/zh-Hant/grafana.json | 47 +++++++++++++++++++++++++++++ 18 files changed, 846 insertions(+) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 52d56bb948c..4e8a700bc26 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -11906,7 +11906,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "Odstranit", "confirm-delete-keep-resources": "Opravdu chcete odstranit konfiguraci úložiště, ale ponechat jeho zdroje?", "confirm-delete-with-resources": "Opravdu chcete odstranit konfiguraci úložiště a všechny jeho zdroje?", @@ -12174,6 +12220,7 @@ "jobs": "Práce" }, "repository-actions": { + "connections": "", "settings": "Nastavení", "source-code": "Zdrojový kód" }, diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 616960f15e7..1613801e0af 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -11806,7 +11806,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "Löschen", "confirm-delete-keep-resources": "Sind Sie sicher, dass Sie die Repository-Konfiguration löschen, aber ihre Ressourcen behalten möchten?", "confirm-delete-with-resources": "Sind Sie sicher, dass Sie die Repository-Konfiguration und alle ihre Ressourcen löschen möchten?", @@ -12070,6 +12116,7 @@ "jobs": "Aufträge" }, "repository-actions": { + "connections": "", "settings": "Einstellungen", "source-code": "Quellcode" }, diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index e4a00ce24fe..e94cd155216 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -11806,7 +11806,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "Eliminar", "confirm-delete-keep-resources": "¿Seguro que quieres eliminar la configuración del repositorio pero conservar sus recursos?", "confirm-delete-with-resources": "¿Seguro que quieres eliminar la configuración del repositorio y todos sus recursos?", @@ -12070,6 +12116,7 @@ "jobs": "Trabajos" }, "repository-actions": { + "connections": "", "settings": "Configuración", "source-code": "Código fuente" }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 7d1ad65f31a..0954b9d56b2 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -11806,7 +11806,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "Supprimer", "confirm-delete-keep-resources": "Voulez-vous vraiment supprimer la configuration du référentiel tout en conservant ses ressources ?", "confirm-delete-with-resources": "Voulez-vous vraiment supprimer la configuration du référentiel ainsi que toutes ses ressources ?", @@ -12070,6 +12116,7 @@ "jobs": "Missions" }, "repository-actions": { + "connections": "", "settings": "Paramètres", "source-code": "Code source" }, diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 88e927039ac..deb86e3a541 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -11806,7 +11806,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "Törlés", "confirm-delete-keep-resources": "Biztosan törli az adattár konfigurációját, és megtartja az erőforrásait?", "confirm-delete-with-resources": "Biztosan törli az adattár konfigurációját és az összes erőforrását?", @@ -12070,6 +12116,7 @@ "jobs": "Feladatok" }, "repository-actions": { + "connections": "", "settings": "Beállítások", "source-code": "Forráskód" }, diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index df8ee95d6aa..d0aea63ab48 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -11756,7 +11756,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "Hapus", "confirm-delete-keep-resources": "Anda yakin ingin menghapus konfigurasi repositori, tetapi menyimpan sumber dayanya?", "confirm-delete-with-resources": "Anda yakin ingin menghapus konfigurasi repositori dan semua sumber dayanya?", @@ -12018,6 +12064,7 @@ "jobs": "Pekerjaan" }, "repository-actions": { + "connections": "", "settings": "Pengaturan", "source-code": "Kode sumber" }, diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 554727b9577..8b92ef5753d 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -11806,7 +11806,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "Elimina", "confirm-delete-keep-resources": "Vuoi davvero eliminare la configurazione del repository ma conservarne le risorse?", "confirm-delete-with-resources": "Vuoi davvero eliminare la configurazione del repository e tutte le sue risorse?", @@ -12070,6 +12116,7 @@ "jobs": "Attività" }, "repository-actions": { + "connections": "", "settings": "Impostazioni", "source-code": "Codice sorgente" }, diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 16cd9eb957c..376bd220001 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -11756,7 +11756,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "削除", "confirm-delete-keep-resources": "リポジトリ設定を削除するものの、そのリソースを保持してもよろしいですか?", "confirm-delete-with-resources": "リポジトリ設定とそのすべてのリソースを削除してもよろしいですか?", @@ -12018,6 +12064,7 @@ "jobs": "ジョブ" }, "repository-actions": { + "connections": "", "settings": "設定", "source-code": "ソースコード" }, diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 2ea09fb8471..836406edf6a 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -11756,7 +11756,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "삭제", "confirm-delete-keep-resources": "정말 리포지토리 구성만 삭제하고 해당 리소스는 그대로 유지하시겠어요?", "confirm-delete-with-resources": "정말 리포지토리 구성과 해당하는 모든 리소스를 삭제하시겠어요?", @@ -12018,6 +12064,7 @@ "jobs": "작업" }, "repository-actions": { + "connections": "", "settings": "설정", "source-code": "소스 코드" }, diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 7cf226932bc..aa02d1f7445 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -11806,7 +11806,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "Verwijderen", "confirm-delete-keep-resources": "Weet je zeker dat je de repository-configuratie wilt verwijderen, maar de bronnen wilt behouden?", "confirm-delete-with-resources": "Weet je zeker dat je de repository-configuratie en alle bronnen wilt verwijderen?", @@ -12070,6 +12116,7 @@ "jobs": "Taken" }, "repository-actions": { + "connections": "", "settings": "Instellingen", "source-code": "Broncode" }, diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 4cf8c601bfc..715da5ac969 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -11906,7 +11906,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "Usuń", "confirm-delete-keep-resources": "Na pewno chcesz usunąć konfigurację repozytorium, ale zachować jego zasoby?", "confirm-delete-with-resources": "Na pewno chcesz usunąć konfigurację repozytorium i wszystkie jego zasoby?", @@ -12174,6 +12220,7 @@ "jobs": "Zadania" }, "repository-actions": { + "connections": "", "settings": "Ustawienia", "source-code": "Kod źródłowy" }, diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index aeb840530ca..c91b4cc89cc 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -11806,7 +11806,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "Excluir", "confirm-delete-keep-resources": "Tem certeza de que deseja excluir a configuração do repositório, mas manter seus recursos?", "confirm-delete-with-resources": "Tem certeza de que deseja excluir a configuração do repositório e todos os recursos dele?", @@ -12070,6 +12116,7 @@ "jobs": "Tarefas" }, "repository-actions": { + "connections": "", "settings": "Configurações", "source-code": "Código fonte" }, diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 2a4c53052d8..42eec6cc55f 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -11806,7 +11806,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "Eliminar", "confirm-delete-keep-resources": "Tem a certeza de que pretende eliminar a configuração do repositório, mas manter os seus recursos?", "confirm-delete-with-resources": "Tem a certeza de que pretende eliminar a configuração do repositório e todos os seus recursos?", @@ -12070,6 +12116,7 @@ "jobs": "Trabalhos" }, "repository-actions": { + "connections": "", "settings": "Definições", "source-code": "Código-fonte" }, diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index 571918fa148..c3f5952cb21 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -11906,7 +11906,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "Удалить", "confirm-delete-keep-resources": "Вы уверены, что хотите удалить конфигурацию репозитория, но сохранить его ресурсы?", "confirm-delete-with-resources": "Вы уверены, что хотите удалить конфигурацию репозитория и все его ресурсы?", @@ -12174,6 +12220,7 @@ "jobs": "Задания" }, "repository-actions": { + "connections": "", "settings": "Параметры", "source-code": "Исходный код" }, diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 27b318f57e3..aaa8f3197c1 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -11806,7 +11806,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "Ta bort", "confirm-delete-keep-resources": "Är du säker på att du vill radera lagringsplatskonfigurationen men behålla dess resurser?", "confirm-delete-with-resources": "Är du säker på att du vill radera lagringsplatskonfigurationen och alla dess resurser?", @@ -12070,6 +12116,7 @@ "jobs": "Jobb" }, "repository-actions": { + "connections": "", "settings": "Inställningar", "source-code": "Källkod" }, diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 59d394e89a2..11425218c88 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -11806,7 +11806,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "Sil", "confirm-delete-keep-resources": "", "confirm-delete-with-resources": "", @@ -12070,6 +12116,7 @@ "jobs": "İşler" }, "repository-actions": { + "connections": "", "settings": "Ayarlar", "source-code": "Kaynak kodu" }, diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 2a3c4b999d7..4a2a27e1ef4 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -11756,7 +11756,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "删除", "confirm-delete-keep-resources": "您确定要删除存储库配置但保留其资源吗?", "confirm-delete-with-resources": "您确定要删除存储库配置及其所有资源吗?", @@ -12018,6 +12064,7 @@ "jobs": "作业" }, "repository-actions": { + "connections": "", "settings": "设置", "source-code": "源代码" }, diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 246ad78138e..331313dccce 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -11756,7 +11756,53 @@ "free-tier-limit-tooltip": "", "instance-fully-managed-tooltip": "" }, + "connection-form": { + "alert-connection-deleted": "", + "alert-connection-saved": "", + "alert-connection-updated": "", + "back-to-connections": "", + "button-save": "", + "button-saving": "", + "description-app-id": "", + "description-installation-id": "", + "description-private-key": "", + "description-provider": "", + "error-delete-connection": "", + "error-required": "", + "error-save-connection": "", + "label-app-id": "", + "label-installation-id": "", + "label-private-key": "", + "label-provider": "", + "not-found": "", + "not-found-description": "", + "page-subtitle": "", + "page-title-create": "", + "page-title-edit": "", + "placeholder-app-id": "", + "placeholder-installation-id": "", + "placeholder-private-key": "" + }, + "connections": { + "add-connection": "", + "cancel": "", + "delete": "", + "delete-confirm": "", + "delete-title": "", + "error-loading": "", + "no-connections": "", + "no-connections-message": "", + "no-results": "", + "page-subtitle": "", + "page-title": "", + "search-placeholder": "", + "status-connected": "", + "status-disconnected": "", + "status-unknown": "", + "view": "" + }, "delete-repository-button": { + "button-cancel": "", "button-delete": "刪除", "confirm-delete-keep-resources": "確定要刪除儲存庫設定,但保留其資源嗎?", "confirm-delete-with-resources": "確定要刪除儲存庫設定及其所有資源嗎?", @@ -12018,6 +12064,7 @@ "jobs": "作業" }, "repository-actions": { + "connections": "", "settings": "設定", "source-code": "原始碼" }, From bd0140b6f0e62b019c182be768fd370be5a20563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 14 Jan 2026 06:30:05 +0100 Subject: [PATCH 19/19] GrafanaBootData: Deprecate config.apps (#115610) * GrafanaBootData: decouple `config.apps` from boot data IV * chore: changed to openfeature flags eval * chore: updates after PR feedback * chore: updates after PR feedback * chore: copy types to runtime package * chore: add code ownership * chore: deprecate in interface too * chore: add important notice to comments * chore: deprecate the whole interface --- .github/CODEOWNERS | 1 + apps/plugins/Makefile | 13 +- apps/plugins/README.md | 3 +- apps/plugins/kinds/manifest.cue | 2 +- .../meta/v0alpha1/meta_object_gen.ts | 49 + .../meta/v0alpha1/types.metadata.gen.ts | 30 + .../generated/meta/v0alpha1/types.spec.gen.ts | 278 ++ .../meta/v0alpha1/types.status.gen.ts | 30 + .../plugin/v0alpha1/plugin_object_gen.ts | 49 + .../plugin/v0alpha1/types.metadata.gen.ts | 30 + .../plugin/v0alpha1/types.spec.gen.ts | 13 + .../plugin/v0alpha1/types.status.gen.ts | 30 + eslint-suppressions.json | 128 + eslint.config.js | 38 + packages/grafana-data/src/types/config.ts | 2 + packages/grafana-data/src/types/plugin.ts | 1 + packages/grafana-runtime/src/config.ts | 1 + packages/grafana-runtime/src/index.ts | 2 + .../grafana-runtime/src/internal/index.ts | 2 + .../src/services/pluginMeta/apps.test.ts | 258 + .../src/services/pluginMeta/apps.ts | 71 + .../src/services/pluginMeta/hooks.test.tsx | 214 + .../src/services/pluginMeta/hooks.tsx | 35 + .../services/pluginMeta/mappers/mappers.ts | 7 + .../mappers/v0alpha1AppMapper.test.ts | 84 + .../pluginMeta/mappers/v0alpha1AppMapper.ts | 111 + .../src/services/pluginMeta/plugins.test.ts | 153 + .../src/services/pluginMeta/plugins.ts | 41 + .../pluginMeta/test-fixtures/config.apps.ts | 303 ++ .../test-fixtures/v0alpha1Response.ts | 4378 +++++++++++++++++ .../src/services/pluginMeta/types.ts | 10 + .../pluginMeta/types/meta_object_gen.ts | 49 + .../pluginMeta/types/types.spec.gen.ts | 278 ++ .../pluginMeta/types/types.status.gen.ts | 30 + 34 files changed, 6718 insertions(+), 6 deletions(-) create mode 100644 apps/plugins/plugin/src/generated/meta/v0alpha1/meta_object_gen.ts create mode 100644 apps/plugins/plugin/src/generated/meta/v0alpha1/types.metadata.gen.ts create mode 100644 apps/plugins/plugin/src/generated/meta/v0alpha1/types.spec.gen.ts create mode 100644 apps/plugins/plugin/src/generated/meta/v0alpha1/types.status.gen.ts create mode 100644 apps/plugins/plugin/src/generated/plugin/v0alpha1/plugin_object_gen.ts create mode 100644 apps/plugins/plugin/src/generated/plugin/v0alpha1/types.metadata.gen.ts create mode 100644 apps/plugins/plugin/src/generated/plugin/v0alpha1/types.spec.gen.ts create mode 100644 apps/plugins/plugin/src/generated/plugin/v0alpha1/types.status.gen.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/apps.test.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/apps.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/hooks.test.tsx create mode 100644 packages/grafana-runtime/src/services/pluginMeta/hooks.tsx create mode 100644 packages/grafana-runtime/src/services/pluginMeta/mappers/mappers.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/mappers/v0alpha1AppMapper.test.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/mappers/v0alpha1AppMapper.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/plugins.test.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/plugins.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/test-fixtures/config.apps.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/test-fixtures/v0alpha1Response.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/types.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/types/meta_object_gen.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/types/types.spec.gen.ts create mode 100644 packages/grafana-runtime/src/services/pluginMeta/types/types.status.gen.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ca9de90ba02..0dda8519ef6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -658,6 +658,7 @@ i18next.config.ts @grafana/grafana-frontend-platform /packages/grafana-runtime/src/services/LocationService.tsx @grafana/grafana-search-navigate-organise /packages/grafana-runtime/src/services/LocationSrv.ts @grafana/grafana-search-navigate-organise /packages/grafana-runtime/src/services/live.ts @grafana/dashboards-squad +/packages/grafana-runtime/src/services/pluginMeta @grafana/plugins-platform-frontend /packages/grafana-runtime/src/utils/chromeHeaderHeight.ts @grafana/grafana-search-navigate-organise /packages/grafana-runtime/src/utils/DataSourceWithBackend* @grafana/grafana-datasources-core-services /packages/grafana-runtime/src/utils/licensing.ts @grafana/grafana-operator-experience-squad diff --git a/apps/plugins/Makefile b/apps/plugins/Makefile index 230bfd4149a..2db266ef19b 100644 --- a/apps/plugins/Makefile +++ b/apps/plugins/Makefile @@ -1,9 +1,16 @@ include ../sdk.mk -.PHONY: generate # Run Grafana App SDK code generation -generate: install-app-sdk update-app-sdk +.PHONY: internal-generate # Run Grafana App SDK code generation +internal-generate: install-app-sdk update-app-sdk @$(APP_SDK_BIN) generate \ --source=./kinds/ \ --gogenpath=./pkg/apis \ --grouping=group \ - --defencoding=none \ No newline at end of file + --defencoding=none + +.PHONY: generate +generate: internal-generate # copy files to packages/grafana-runtime/src/services/pluginMeta/types + rm -f ./packages/grafana-runtime/src/services/pluginMeta/types/*.ts + cp plugin/src/generated/meta/v0alpha1/meta_object_gen.ts ../../packages/grafana-runtime/src/services/pluginMeta/types/meta_object_gen.ts + cp plugin/src/generated/meta/v0alpha1/types.spec.gen.ts ../../packages/grafana-runtime/src/services/pluginMeta/types/types.spec.gen.ts + cp plugin/src/generated/meta/v0alpha1/types.status.gen.ts ../../packages/grafana-runtime/src/services/pluginMeta/types/types.status.gen.ts \ No newline at end of file diff --git a/apps/plugins/README.md b/apps/plugins/README.md index 7f91dd6ea12..f21fa6701b5 100644 --- a/apps/plugins/README.md +++ b/apps/plugins/README.md @@ -4,8 +4,7 @@ API documentation is available at http://localhost:3000/swagger?api=plugins.graf ## Codegen -- Go: `make generate` -- Frontend: Follow instructions in this [README](../..//packages/grafana-api-clients/README.md) +- Go and TypeScript: `make generate` ## Plugin sync diff --git a/apps/plugins/kinds/manifest.cue b/apps/plugins/kinds/manifest.cue index f624dc117bc..680a0f7565d 100644 --- a/apps/plugins/kinds/manifest.cue +++ b/apps/plugins/kinds/manifest.cue @@ -11,7 +11,7 @@ manifest: { v0alpha1Version: { served: true codegen: { - ts: {enabled: false} + ts: {enabled: true} go: {enabled: true} } kinds: [ diff --git a/apps/plugins/plugin/src/generated/meta/v0alpha1/meta_object_gen.ts b/apps/plugins/plugin/src/generated/meta/v0alpha1/meta_object_gen.ts new file mode 100644 index 00000000000..044ec1f4cd8 --- /dev/null +++ b/apps/plugins/plugin/src/generated/meta/v0alpha1/meta_object_gen.ts @@ -0,0 +1,49 @@ +/* + * This file was generated by grafana-app-sdk. DO NOT EDIT. + */ +import { Spec } from './types.spec.gen'; +import { Status } from './types.status.gen'; + +export interface Metadata { + name: string; + namespace: string; + generateName?: string; + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + deletionTimestamp?: string; + deletionGracePeriodSeconds?: number; + labels?: Record; + annotations?: Record; + ownerReferences?: OwnerReference[]; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface OwnerReference { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +} + +export interface ManagedFieldsEntry { + manager?: string; + operation?: string; + apiVersion?: string; + time?: string; + fieldsType?: string; + subresource?: string; +} + +export interface Meta { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; + status: Status; +} diff --git a/apps/plugins/plugin/src/generated/meta/v0alpha1/types.metadata.gen.ts b/apps/plugins/plugin/src/generated/meta/v0alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/plugins/plugin/src/generated/meta/v0alpha1/types.metadata.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +export interface Metadata { + updateTimestamp: string; + createdBy: string; + uid: string; + creationTimestamp: string; + deletionTimestamp?: string; + finalizers: string[]; + resourceVersion: string; + generation: number; + updatedBy: string; + labels: Record; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/apps/plugins/plugin/src/generated/meta/v0alpha1/types.spec.gen.ts b/apps/plugins/plugin/src/generated/meta/v0alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..51845e98454 --- /dev/null +++ b/apps/plugins/plugin/src/generated/meta/v0alpha1/types.spec.gen.ts @@ -0,0 +1,278 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// JSON configuration schema for Grafana plugins +// Converted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json +export interface JSONData { + // Unique name of the plugin + id: string; + // Plugin type + type: "app" | "datasource" | "panel" | "renderer"; + // Human-readable name of the plugin + name: string; + // Metadata for the plugin + info: Info; + // Dependency information + dependencies: Dependencies; + // Optional fields + alerting?: boolean; + annotations?: boolean; + autoEnabled?: boolean; + backend?: boolean; + buildMode?: string; + builtIn?: boolean; + category?: "tsdb" | "logging" | "cloud" | "tracing" | "profiling" | "sql" | "enterprise" | "iot" | "other"; + enterpriseFeatures?: EnterpriseFeatures; + executable?: string; + hideFromList?: boolean; + // +listType=atomic + includes?: Include[]; + logs?: boolean; + metrics?: boolean; + multiValueFilterOperators?: boolean; + pascalName?: string; + preload?: boolean; + queryOptions?: QueryOptions; + // +listType=atomic + routes?: Route[]; + skipDataQuery?: boolean; + state?: "alpha" | "beta"; + streaming?: boolean; + suggestions?: boolean; + tracing?: boolean; + iam?: IAM; + // +listType=atomic + roles?: Role[]; + extensions?: Extensions; +} + +export const defaultJSONData = (): JSONData => ({ + id: "", + type: "app", + name: "", + info: defaultInfo(), + dependencies: defaultDependencies(), +}); + +export interface Info { + // Required fields + // +listType=set + keywords: string[]; + logos: { + small: string; + large: string; + }; + updated: string; + version: string; + // Optional fields + author?: { + name?: string; + email?: string; + url?: string; + }; + description?: string; + // +listType=atomic + links?: { + name?: string; + url?: string; + }[]; + // +listType=atomic + screenshots?: { + name?: string; + path?: string; + }[]; +} + +export const defaultInfo = (): Info => ({ + keywords: [], + logos: { + small: "", + large: "", +}, + updated: "", + version: "", +}); + +export interface Dependencies { + // Required field + grafanaDependency: string; + // Optional fields + grafanaVersion?: string; + // +listType=set + // +listMapKey=id + plugins?: { + id: string; + type: "app" | "datasource" | "panel"; + name: string; + }[]; + extensions?: { + // +listType=set + exposedComponents?: string[]; + }; +} + +export const defaultDependencies = (): Dependencies => ({ + grafanaDependency: "", +}); + +export interface EnterpriseFeatures { + // Allow additional properties + healthDiagnosticsErrors?: boolean; +} + +export const defaultEnterpriseFeatures = (): EnterpriseFeatures => ({ + healthDiagnosticsErrors: false, +}); + +export interface Include { + uid?: string; + type?: "dashboard" | "page" | "panel" | "datasource"; + name?: string; + component?: string; + role?: "Admin" | "Editor" | "Viewer" | "None"; + action?: string; + path?: string; + addToNav?: boolean; + defaultNav?: boolean; + icon?: string; +} + +export const defaultInclude = (): Include => ({ +}); + +export interface QueryOptions { + maxDataPoints?: boolean; + minInterval?: boolean; + cacheTimeout?: boolean; +} + +export const defaultQueryOptions = (): QueryOptions => ({ +}); + +export interface Route { + path?: string; + method?: string; + url?: string; + reqSignedIn?: boolean; + reqRole?: string; + reqAction?: string; + // +listType=atomic + headers?: string[]; + body?: Record; + tokenAuth?: { + url?: string; + // +listType=set + scopes?: string[]; + params?: Record; + }; + jwtTokenAuth?: { + url?: string; + // +listType=set + scopes?: string[]; + params?: Record; + }; + // +listType=atomic + urlParams?: { + name?: string; + content?: string; + }[]; +} + +export const defaultRoute = (): Route => ({ +}); + +export interface IAM { + // +listType=atomic + permissions?: { + action?: string; + scope?: string; + }[]; +} + +export const defaultIAM = (): IAM => ({ +}); + +export interface Role { + role?: { + name?: string; + description?: string; + // +listType=atomic + permissions?: { + action?: string; + scope?: string; + }[]; + }; + // +listType=set + grants?: string[]; +} + +export const defaultRole = (): Role => ({ +}); + +export interface Extensions { + // +listType=atomic + addedComponents?: { + // +listType=set + targets: string[]; + title: string; + description?: string; + }[]; + // +listType=atomic + addedLinks?: { + // +listType=set + targets: string[]; + title: string; + description?: string; + }[]; + // +listType=atomic + addedFunctions?: { + // +listType=set + targets: string[]; + title: string; + description?: string; + }[]; + // +listType=set + // +listMapKey=id + exposedComponents?: { + id: string; + title?: string; + description?: string; + }[]; + // +listType=set + // +listMapKey=id + extensionPoints?: { + id: string; + title?: string; + description?: string; + }[]; +} + +export const defaultExtensions = (): Extensions => ({ +}); + +export interface Spec { + pluginJson: JSONData; + class: "core" | "external"; + module?: { + path: string; + hash?: string; + loadingStrategy?: "fetch" | "script"; + }; + baseURL?: string; + signature?: { + status: "internal" | "valid" | "invalid" | "modified" | "unsigned"; + type?: "grafana" | "commercial" | "community" | "private" | "private-glob"; + org?: string; + }; + angular?: { + detected: boolean; + }; + translations?: Record; + // +listType=atomic + children?: string[]; +} + +export const defaultSpec = (): Spec => ({ + pluginJson: defaultJSONData(), + class: "core", +}); + diff --git a/apps/plugins/plugin/src/generated/meta/v0alpha1/types.status.gen.ts b/apps/plugins/plugin/src/generated/meta/v0alpha1/types.status.gen.ts new file mode 100644 index 00000000000..01be8df7961 --- /dev/null +++ b/apps/plugins/plugin/src/generated/meta/v0alpha1/types.status.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface OperatorState { + // lastEvaluation is the ResourceVersion last evaluated + lastEvaluation: string; + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + state: "success" | "in_progress" | "failed"; + // descriptiveState is an optional more descriptive state field which has no requirements on format + descriptiveState?: string; + // details contains any extra information that is operator-specific + details?: Record; +} + +export const defaultOperatorState = (): OperatorState => ({ + lastEvaluation: "", + state: "success", +}); + +export interface Status { + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + operatorStates?: Record; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ +}); + diff --git a/apps/plugins/plugin/src/generated/plugin/v0alpha1/plugin_object_gen.ts b/apps/plugins/plugin/src/generated/plugin/v0alpha1/plugin_object_gen.ts new file mode 100644 index 00000000000..c4e625fc418 --- /dev/null +++ b/apps/plugins/plugin/src/generated/plugin/v0alpha1/plugin_object_gen.ts @@ -0,0 +1,49 @@ +/* + * This file was generated by grafana-app-sdk. DO NOT EDIT. + */ +import { Spec } from './types.spec.gen'; +import { Status } from './types.status.gen'; + +export interface Metadata { + name: string; + namespace: string; + generateName?: string; + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + deletionTimestamp?: string; + deletionGracePeriodSeconds?: number; + labels?: Record; + annotations?: Record; + ownerReferences?: OwnerReference[]; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface OwnerReference { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +} + +export interface ManagedFieldsEntry { + manager?: string; + operation?: string; + apiVersion?: string; + time?: string; + fieldsType?: string; + subresource?: string; +} + +export interface Plugin { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; + status: Status; +} diff --git a/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.metadata.gen.ts b/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.metadata.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +export interface Metadata { + updateTimestamp: string; + createdBy: string; + uid: string; + creationTimestamp: string; + deletionTimestamp?: string; + finalizers: string[]; + resourceVersion: string; + generation: number; + updatedBy: string; + labels: Record; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.spec.gen.ts b/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..6b7824b8941 --- /dev/null +++ b/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.spec.gen.ts @@ -0,0 +1,13 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface Spec { + id: string; + version: string; + url?: string; +} + +export const defaultSpec = (): Spec => ({ + id: "", + version: "", +}); + diff --git a/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.status.gen.ts b/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.status.gen.ts new file mode 100644 index 00000000000..01be8df7961 --- /dev/null +++ b/apps/plugins/plugin/src/generated/plugin/v0alpha1/types.status.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface OperatorState { + // lastEvaluation is the ResourceVersion last evaluated + lastEvaluation: string; + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + state: "success" | "in_progress" | "failed"; + // descriptiveState is an optional more descriptive state field which has no requirements on format + descriptiveState?: string; + // details contains any extra information that is operator-specific + details?: Record; +} + +export const defaultOperatorState = (): OperatorState => ({ + lastEvaluation: "", + state: "success", +}); + +export interface Status { + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + operatorStates?: Record; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ +}); + diff --git a/eslint-suppressions.json b/eslint-suppressions.json index f633ed5b4eb..70df9a82829 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1337,6 +1337,11 @@ "count": 2 } }, + "public/app/features/alerting/unified/api/onCallApi.test.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, "public/app/features/alerting/unified/components/AnnotationDetailsField.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 1 @@ -1377,6 +1382,11 @@ "count": 1 } }, + "public/app/features/alerting/unified/components/import-to-gma/ConfirmConvertModal.test.tsx": { + "no-restricted-syntax": { + "count": 1 + } + }, "public/app/features/alerting/unified/components/import-to-gma/NamespaceAndGroupFilter.tsx": { "no-restricted-syntax": { "count": 2 @@ -1617,11 +1627,31 @@ "count": 1 } }, + "public/app/features/alerting/unified/mocks/server/configure.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "public/app/features/alerting/unified/mocks/server/handlers/plugins.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "public/app/features/alerting/unified/rule-editor/clone.utils.test.tsx": { + "no-restricted-syntax": { + "count": 2 + } + }, "public/app/features/alerting/unified/rule-editor/formDefaults.ts": { "no-restricted-syntax": { "count": 6 } }, + "public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "public/app/features/alerting/unified/types/alerting.ts": { "@typescript-eslint/no-explicit-any": { "count": 5 @@ -1632,6 +1662,16 @@ "count": 1 } }, + "public/app/features/alerting/unified/utils/config.test.ts": { + "no-restricted-syntax": { + "count": 6 + } + }, + "public/app/features/alerting/unified/utils/config.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "public/app/features/alerting/unified/utils/datasource.ts": { "no-restricted-syntax": { "count": 2 @@ -1663,12 +1703,20 @@ "count": 1 } }, + "public/app/features/alerting/unified/utils/rules.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "public/app/features/alerting/unified/utils/rules.ts": { "@typescript-eslint/consistent-type-assertions": { "count": 3 }, "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "no-restricted-syntax": { + "count": 1 } }, "public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx": { @@ -1724,6 +1772,16 @@ "count": 1 } }, + "public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.test.tsx": { + "no-restricted-syntax": { + "count": 2 + } + }, + "public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.tsx": { + "no-restricted-syntax": { + "count": 1 + } + }, "public/app/features/connections/tabs/ConnectData/ConnectData.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 1 @@ -2063,6 +2121,11 @@ "count": 1 } }, + "public/app/features/dashboard/components/GenAI/utils.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx": { "no-restricted-syntax": { "count": 3 @@ -2889,6 +2952,71 @@ "count": 1 } }, + "public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts": { + "no-restricted-syntax": { + "count": 6 + } + }, + "public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts": { + "no-restricted-syntax": { + "count": 6 + } + }, + "public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts": { + "no-restricted-syntax": { + "count": 6 + } + }, + "public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.test.ts": { + "no-restricted-syntax": { + "count": 6 + } + }, + "public/app/features/plugins/extensions/usePluginComponent.test.tsx": { + "no-restricted-syntax": { + "count": 3 + } + }, + "public/app/features/plugins/extensions/usePluginComponents.test.tsx": { + "no-restricted-syntax": { + "count": 2 + } + }, + "public/app/features/plugins/extensions/usePluginFunctions.test.tsx": { + "no-restricted-syntax": { + "count": 2 + } + }, + "public/app/features/plugins/extensions/usePluginLinks.test.tsx": { + "no-restricted-syntax": { + "count": 2 + } + }, + "public/app/features/plugins/extensions/utils.test.tsx": { + "no-restricted-syntax": { + "count": 27 + } + }, + "public/app/features/plugins/extensions/utils.tsx": { + "no-restricted-syntax": { + "count": 7 + } + }, + "public/app/features/plugins/extensions/validators.test.tsx": { + "no-restricted-syntax": { + "count": 30 + } + }, + "public/app/features/plugins/extensions/validators.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, + "public/app/features/plugins/sandbox/codeLoader.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "public/app/features/plugins/sandbox/distortions.ts": { "@typescript-eslint/consistent-type-assertions": { "count": 1 diff --git a/eslint.config.js b/eslint.config.js index 5e44ffebaf4..479f11aac66 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -117,6 +117,8 @@ module.exports = [ 'scripts/grafana-server/tmp', 'packages/grafana-ui/src/graveyard', // deprecated UI components slated for removal 'public/build-swagger', // swagger build output + 'apps/plugins/plugin/src/generated/meta/v0alpha1', + 'apps/plugins/plugin/src/generated/plugin/v0alpha1', ], }, ...grafanaConfig, @@ -575,6 +577,42 @@ module.exports = [ "Property[key.name='a11y'][value.type='ObjectExpression'] Property[key.name='test'][value.value='off']", message: 'Skipping a11y tests is not allowed. Please fix the component or story instead.', }, + { + selector: 'MemberExpression[object.name="config"][property.name="apps"]', + message: + 'Usage of config.apps is not allowed. Use the function getAppPluginMetas or useAppPluginMetas from @grafana/runtime instead', + }, + ], + }, + }, + { + files: [...commonTestIgnores], + ignores: [ + // FIXME: Remove once all enterprise issues are fixed - + // we don't have a suppressions file/approach for enterprise code yet + ...enterpriseIgnores, + ], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'MemberExpression[object.name="config"][property.name="apps"]', + message: + 'Usage of config.apps is not allowed. Use the function getAppPluginMetas or useAppPluginMetas from @grafana/runtime instead', + }, + ], + }, + }, + { + files: [...enterpriseIgnores], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'MemberExpression[object.name="config"][property.name="apps"]', + message: + 'Usage of config.apps is not allowed. Use the function getAppPluginMetas or useAppPluginMetas from @grafana/runtime instead', + }, ], }, }, diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index b2d3c16a3b1..922d9273699 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -32,6 +32,7 @@ export type AppPluginConfig = { path: string; version: string; preload: boolean; + /** @deprecated it will be removed in a future release */ angular: AngularMeta; loadingStrategy: PluginLoadingStrategy; dependencies: PluginDependencies; @@ -219,6 +220,7 @@ export interface GrafanaConfig { snapshotEnabled: boolean; datasources: { [str: string]: DataSourceInstanceSettings }; panels: { [key: string]: PanelPluginMeta }; + /** @deprecated it will be removed in a future release */ apps: Record; auth: AuthSettings; minRefreshInterval: string; diff --git a/packages/grafana-data/src/types/plugin.ts b/packages/grafana-data/src/types/plugin.ts index 045dfdcee0b..8b96ac8f70f 100644 --- a/packages/grafana-data/src/types/plugin.ts +++ b/packages/grafana-data/src/types/plugin.ts @@ -53,6 +53,7 @@ export interface PluginError { pluginType?: PluginType; } +/** @deprecated it will be removed in a future release */ export interface AngularMeta { detected: boolean; hideDeprecation: boolean; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 18cce14f236..99809235cab 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -86,6 +86,7 @@ export class GrafanaBootConfig { snapshotEnabled = true; datasources: { [str: string]: DataSourceInstanceSettings } = {}; panels: { [key: string]: PanelPluginMeta } = {}; + /** @deprecated it will be removed in a future release, use isAppPluginInstalled or getAppPluginVersion instead */ apps: Record = {}; auth: AuthSettings = {}; minRefreshInterval = ''; diff --git a/packages/grafana-runtime/src/index.ts b/packages/grafana-runtime/src/index.ts index 58b30be8542..380b87fee7d 100644 --- a/packages/grafana-runtime/src/index.ts +++ b/packages/grafana-runtime/src/index.ts @@ -77,3 +77,5 @@ export { getCorrelationsService, setCorrelationsService, } from './services/CorrelationsService'; +export { getAppPluginVersion, isAppPluginInstalled } from './services/pluginMeta/apps'; +export { useAppPluginInstalled, useAppPluginVersion } from './services/pluginMeta/hooks'; diff --git a/packages/grafana-runtime/src/internal/index.ts b/packages/grafana-runtime/src/internal/index.ts index aed6b86ebfb..fa13873c094 100644 --- a/packages/grafana-runtime/src/internal/index.ts +++ b/packages/grafana-runtime/src/internal/index.ts @@ -29,3 +29,5 @@ export { export { UserStorage } from '../utils/userStorage'; export { initOpenFeature, evaluateBooleanFlag } from './openFeature'; +export { getAppPluginMeta, getAppPluginMetas, setAppPluginMetas } from '../services/pluginMeta/apps'; +export { useAppPluginMeta, useAppPluginMetas } from '../services/pluginMeta/hooks'; diff --git a/packages/grafana-runtime/src/services/pluginMeta/apps.test.ts b/packages/grafana-runtime/src/services/pluginMeta/apps.test.ts new file mode 100644 index 00000000000..554917041cc --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/apps.test.ts @@ -0,0 +1,258 @@ +import { evaluateBooleanFlag } from '../../internal/openFeature'; + +import { + getAppPluginMeta, + getAppPluginMetas, + getAppPluginVersion, + isAppPluginInstalled, + setAppPluginMetas, +} from './apps'; +import { initPluginMetas } from './plugins'; +import { app } from './test-fixtures/config.apps'; + +jest.mock('./plugins', () => ({ ...jest.requireActual('./plugins'), initPluginMetas: jest.fn() })); +jest.mock('../../internal/openFeature', () => ({ + ...jest.requireActual('../../internal/openFeature'), + evaluateBooleanFlag: jest.fn(), +})); + +const initPluginMetasMock = jest.mocked(initPluginMetas); +const evaluateBooleanFlagMock = jest.mocked(evaluateBooleanFlag); + +describe('when useMTPlugins flag is enabled and apps is not initialized', () => { + beforeEach(() => { + setAppPluginMetas({}); + jest.resetAllMocks(); + initPluginMetasMock.mockResolvedValue({ items: [] }); + evaluateBooleanFlagMock.mockReturnValue(true); + }); + + it('getAppPluginMetas should call initPluginMetas and return correct result', async () => { + const apps = await getAppPluginMetas(); + + expect(apps).toEqual([]); + expect(initPluginMetasMock).toHaveBeenCalledTimes(1); + }); + + it('getAppPluginMeta should call initPluginMetas and return correct result', async () => { + const result = await getAppPluginMeta('myorg-someplugin-app'); + + expect(result).toEqual(null); + expect(initPluginMetasMock).toHaveBeenCalledTimes(1); + }); + + it('isAppPluginInstalled should call initPluginMetas and return false', async () => { + const installed = await isAppPluginInstalled('myorg-someplugin-app'); + + expect(installed).toEqual(false); + expect(initPluginMetasMock).toHaveBeenCalledTimes(1); + }); + + it('getAppPluginVersion should call initPluginMetas and return null', async () => { + const result = await getAppPluginVersion('myorg-someplugin-app'); + + expect(result).toEqual(null); + expect(initPluginMetasMock).toHaveBeenCalledTimes(1); + }); +}); + +describe('when useMTPlugins flag is enabled and apps is initialized', () => { + beforeEach(() => { + setAppPluginMetas({ 'myorg-someplugin-app': app }); + jest.resetAllMocks(); + evaluateBooleanFlagMock.mockReturnValue(true); + }); + + it('getAppPluginMetas should not call initPluginMetas and return correct result', async () => { + const apps = await getAppPluginMetas(); + + expect(apps).toEqual([app]); + expect(initPluginMetasMock).not.toHaveBeenCalled(); + }); + + it('getAppPluginMeta should not call initPluginMetas and return correct result', async () => { + const result = await getAppPluginMeta('myorg-someplugin-app'); + + expect(result).toEqual(app); + expect(initPluginMetasMock).not.toHaveBeenCalled(); + }); + + it('getAppPluginMeta should return null if the pluginId is not found', async () => { + const result = await getAppPluginMeta('otherorg-otherplugin-app'); + + expect(result).toEqual(null); + }); + + it('isAppPluginInstalled should not call initPluginMetas and return true', async () => { + const installed = await isAppPluginInstalled('myorg-someplugin-app'); + + expect(installed).toEqual(true); + expect(initPluginMetasMock).not.toHaveBeenCalled(); + }); + + it('isAppPluginInstalled should return false if the pluginId is not found', async () => { + const result = await isAppPluginInstalled('otherorg-otherplugin-app'); + + expect(result).toEqual(false); + }); + + it('getAppPluginVersion should not call initPluginMetas and return correct result', async () => { + const result = await getAppPluginVersion('myorg-someplugin-app'); + + expect(result).toEqual('1.0.0'); + expect(initPluginMetasMock).not.toHaveBeenCalled(); + }); + + it('getAppPluginVersion should return null if the pluginId is not found', async () => { + const result = await getAppPluginVersion('otherorg-otherplugin-app'); + + expect(result).toEqual(null); + }); +}); + +describe('when useMTPlugins flag is disabled and apps is not initialized', () => { + beforeEach(() => { + setAppPluginMetas({}); + jest.resetAllMocks(); + evaluateBooleanFlagMock.mockReturnValue(false); + }); + + it('getAppPluginMetas should not call initPluginMetas and return correct result', async () => { + const apps = await getAppPluginMetas(); + + expect(apps).toEqual([]); + expect(initPluginMetasMock).not.toHaveBeenCalled(); + }); + + it('getAppPluginMeta should not call initPluginMetas and return correct result', async () => { + const result = await getAppPluginMeta('myorg-someplugin-app'); + + expect(result).toEqual(null); + expect(initPluginMetasMock).not.toHaveBeenCalled(); + }); + + it('isAppPluginInstalled should not call initPluginMetas and return false', async () => { + const result = await isAppPluginInstalled('myorg-someplugin-app'); + + expect(result).toEqual(false); + expect(initPluginMetasMock).not.toHaveBeenCalled(); + }); + + it('getAppPluginVersion should not call initPluginMetas and return correct result', async () => { + const result = await getAppPluginVersion('myorg-someplugin-app'); + + expect(result).toEqual(null); + expect(initPluginMetasMock).not.toHaveBeenCalled(); + }); +}); + +describe('when useMTPlugins flag is disabled and apps is initialized', () => { + beforeEach(() => { + setAppPluginMetas({ 'myorg-someplugin-app': app }); + jest.resetAllMocks(); + evaluateBooleanFlagMock.mockReturnValue(false); + }); + + it('getAppPluginMetas should not call initPluginMetas and return correct result', async () => { + const apps = await getAppPluginMetas(); + + expect(apps).toEqual([app]); + expect(initPluginMetasMock).not.toHaveBeenCalled(); + }); + + it('getAppPluginMeta should not call initPluginMetas and return correct result', async () => { + const result = await getAppPluginMeta('myorg-someplugin-app'); + + expect(result).toEqual(app); + expect(initPluginMetasMock).not.toHaveBeenCalled(); + }); + + it('getAppPluginMeta should return null if the pluginId is not found', async () => { + const result = await getAppPluginMeta('otherorg-otherplugin-app'); + + expect(result).toEqual(null); + }); + + it('isAppPluginInstalled should not call initPluginMetas and return true', async () => { + const result = await isAppPluginInstalled('myorg-someplugin-app'); + + expect(result).toEqual(true); + expect(initPluginMetasMock).not.toHaveBeenCalled(); + }); + + it('isAppPluginInstalled should return false if the pluginId is not found', async () => { + const result = await isAppPluginInstalled('otherorg-otherplugin-app'); + + expect(result).toEqual(false); + }); + + it('getAppPluginVersion should not call initPluginMetas and return correct result', async () => { + const result = await getAppPluginVersion('myorg-someplugin-app'); + + expect(result).toEqual('1.0.0'); + expect(initPluginMetasMock).not.toHaveBeenCalled(); + }); + + it('getAppPluginVersion should return null if the pluginId is not found', async () => { + const result = await getAppPluginVersion('otherorg-otherplugin-app'); + + expect(result).toEqual(null); + }); +}); + +describe('immutability', () => { + beforeEach(() => { + setAppPluginMetas({ 'myorg-someplugin-app': app }); + jest.resetAllMocks(); + evaluateBooleanFlagMock.mockReturnValue(false); + }); + + it('getAppPluginMetas should return a deep clone', async () => { + const mutatedApps = await getAppPluginMetas(); + + // assert we have correct props + expect(mutatedApps).toHaveLength(1); + expect(mutatedApps[0].dependencies.grafanaDependency).toEqual('>=10.4.0'); + expect(mutatedApps[0].extensions.addedLinks).toHaveLength(0); + + // mutate deep props + mutatedApps[0].dependencies.grafanaDependency = ''; + mutatedApps[0].extensions.addedLinks.push({ targets: [], title: '', description: '' }); + + // assert we have mutated props + expect(mutatedApps[0].dependencies.grafanaDependency).toEqual(''); + expect(mutatedApps[0].extensions.addedLinks).toHaveLength(1); + expect(mutatedApps[0].extensions.addedLinks[0]).toEqual({ targets: [], title: '', description: '' }); + + const apps = await getAppPluginMetas(); + + // assert that we have not mutated the source + expect(apps[0].dependencies.grafanaDependency).toEqual('>=10.4.0'); + expect(apps[0].extensions.addedLinks).toHaveLength(0); + }); + + it('getAppPluginMeta should return a deep clone', async () => { + const mutatedApp = await getAppPluginMeta('myorg-someplugin-app'); + + // assert we have correct props + expect(mutatedApp).toBeDefined(); + expect(mutatedApp!.dependencies.grafanaDependency).toEqual('>=10.4.0'); + expect(mutatedApp!.extensions.addedLinks).toHaveLength(0); + + // mutate deep props + mutatedApp!.dependencies.grafanaDependency = ''; + mutatedApp!.extensions.addedLinks.push({ targets: [], title: '', description: '' }); + + // assert we have mutated props + expect(mutatedApp!.dependencies.grafanaDependency).toEqual(''); + expect(mutatedApp!.extensions.addedLinks).toHaveLength(1); + expect(mutatedApp!.extensions.addedLinks[0]).toEqual({ targets: [], title: '', description: '' }); + + const result = await getAppPluginMeta('myorg-someplugin-app'); + + // assert that we have not mutated the source + expect(result).toBeDefined(); + expect(result!.dependencies.grafanaDependency).toEqual('>=10.4.0'); + expect(result!.extensions.addedLinks).toHaveLength(0); + }); +}); diff --git a/packages/grafana-runtime/src/services/pluginMeta/apps.ts b/packages/grafana-runtime/src/services/pluginMeta/apps.ts new file mode 100644 index 00000000000..7db359b5a4b --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/apps.ts @@ -0,0 +1,71 @@ +import type { AppPluginConfig } from '@grafana/data'; + +import { config } from '../../config'; +import { evaluateBooleanFlag } from '../../internal/openFeature'; + +import { getAppPluginMapper } from './mappers/mappers'; +import { initPluginMetas } from './plugins'; +import type { AppPluginMetas } from './types'; + +let apps: AppPluginMetas = {}; + +function initialized(): boolean { + return Boolean(Object.keys(apps).length); +} + +async function initAppPluginMetas(): Promise { + if (!evaluateBooleanFlag('useMTPlugins', false)) { + // eslint-disable-next-line no-restricted-syntax + apps = config.apps; + return; + } + + const metas = await initPluginMetas(); + const mapper = getAppPluginMapper(); + apps = mapper(metas); +} + +export async function getAppPluginMetas(): Promise { + if (!initialized()) { + await initAppPluginMetas(); + } + + return Object.values(structuredClone(apps)); +} + +export async function getAppPluginMeta(pluginId: string): Promise { + if (!initialized()) { + await initAppPluginMetas(); + } + + const app = apps[pluginId]; + return app ? structuredClone(app) : null; +} + +/** + * Check if an app plugin is installed. The function does not check if the app plugin is enabled. + * @param pluginId - The id of the app plugin. + * @returns True if the app plugin is installed, false otherwise. + */ +export async function isAppPluginInstalled(pluginId: string): Promise { + const app = await getAppPluginMeta(pluginId); + return Boolean(app); +} + +/** + * Get the version of an app plugin. + * @param pluginId - The id of the app plugin. + * @returns The version of the app plugin, or null if the plugin is not installed. + */ +export async function getAppPluginVersion(pluginId: string): Promise { + const app = await getAppPluginMeta(pluginId); + return app?.version ?? null; +} + +export function setAppPluginMetas(override: AppPluginMetas): void { + if (process.env.NODE_ENV !== 'test') { + throw new Error('setAppPluginMetas() function can only be called from tests.'); + } + + apps = structuredClone(override); +} diff --git a/packages/grafana-runtime/src/services/pluginMeta/hooks.test.tsx b/packages/grafana-runtime/src/services/pluginMeta/hooks.test.tsx new file mode 100644 index 00000000000..1e3c7311118 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/hooks.test.tsx @@ -0,0 +1,214 @@ +import { renderHook, waitFor } from '@testing-library/react'; + +import { + getAppPluginMeta, + getAppPluginMetas, + getAppPluginVersion, + isAppPluginInstalled, + setAppPluginMetas, +} from './apps'; +import { useAppPluginMeta, useAppPluginMetas, useAppPluginInstalled, useAppPluginVersion } from './hooks'; +import { apps } from './test-fixtures/config.apps'; + +const actualApps = jest.requireActual('./apps'); +jest.mock('./apps', () => ({ + ...jest.requireActual('./apps'), + getAppPluginMetas: jest.fn(), + getAppPluginMeta: jest.fn(), + isAppPluginInstalled: jest.fn(), + getAppPluginVersion: jest.fn(), +})); +const getAppPluginMetaMock = jest.mocked(getAppPluginMeta); +const getAppPluginMetasMock = jest.mocked(getAppPluginMetas); +const isAppPluginInstalledMock = jest.mocked(isAppPluginInstalled); +const getAppPluginVersionMock = jest.mocked(getAppPluginVersion); + +describe('useAppPluginMeta', () => { + beforeEach(() => { + setAppPluginMetas(apps); + jest.resetAllMocks(); + getAppPluginMetaMock.mockImplementation(actualApps.getAppPluginMeta); + }); + + it('should return correct default values', async () => { + const { result } = renderHook(() => useAppPluginMeta('grafana-exploretraces-app')); + + expect(result.current.loading).toEqual(true); + expect(result.current.error).toBeUndefined(); + expect(result.current.value).toBeUndefined(); + + await waitFor(() => expect(result.current.loading).toEqual(true)); + }); + + it('should return correct values after loading', async () => { + const { result } = renderHook(() => useAppPluginMeta('grafana-exploretraces-app')); + + await waitFor(() => expect(result.current.loading).toEqual(false)); + + expect(result.current.loading).toEqual(false); + expect(result.current.error).toBeUndefined(); + expect(result.current.value).toEqual(apps['grafana-exploretraces-app']); + }); + + it('should return correct values if the pluginId does not exist', async () => { + const { result } = renderHook(() => useAppPluginMeta('otherorg-otherplugin-app')); + + await waitFor(() => expect(result.current.loading).toEqual(false)); + + expect(result.current.loading).toEqual(false); + expect(result.current.error).toBeUndefined(); + expect(result.current.value).toEqual(null); + }); + + it('should return correct values if useAppPluginMeta throws', async () => { + getAppPluginMetaMock.mockRejectedValue(new Error('Some error')); + + const { result } = renderHook(() => useAppPluginMeta('otherorg-otherplugin-app')); + + await waitFor(() => expect(result.current.loading).toEqual(false)); + + expect(result.current.loading).toEqual(false); + expect(result.current.error).toEqual(new Error('Some error')); + expect(result.current.value).toBeUndefined(); + }); +}); + +describe('useAppPluginMetas', () => { + beforeEach(() => { + setAppPluginMetas(apps); + jest.resetAllMocks(); + getAppPluginMetasMock.mockImplementation(actualApps.getAppPluginMetas); + }); + + it('should return correct default values', async () => { + const { result } = renderHook(() => useAppPluginMetas()); + + expect(result.current.loading).toEqual(true); + expect(result.current.error).toBeUndefined(); + expect(result.current.value).toBeUndefined(); + + await waitFor(() => expect(result.current.loading).toEqual(true)); + }); + + it('should return correct values after loading', async () => { + const { result } = renderHook(() => useAppPluginMetas()); + + await waitFor(() => expect(result.current.loading).toEqual(false)); + + expect(result.current.loading).toEqual(false); + expect(result.current.error).toBeUndefined(); + expect(result.current.value).toEqual(Object.values(apps)); + }); + + it('should return correct values if useAppPluginMetas throws', async () => { + getAppPluginMetasMock.mockRejectedValue(new Error('Some error')); + + const { result } = renderHook(() => useAppPluginMetas()); + + await waitFor(() => expect(result.current.loading).toEqual(false)); + + expect(result.current.loading).toEqual(false); + expect(result.current.error).toEqual(new Error('Some error')); + expect(result.current.value).toBeUndefined(); + }); +}); + +describe('useAppPluginInstalled', () => { + beforeEach(() => { + setAppPluginMetas(apps); + jest.resetAllMocks(); + isAppPluginInstalledMock.mockImplementation(actualApps.isAppPluginInstalled); + }); + + it('should return correct default values', async () => { + const { result } = renderHook(() => useAppPluginInstalled('grafana-exploretraces-app')); + + expect(result.current.loading).toEqual(true); + expect(result.current.error).toBeUndefined(); + expect(result.current.value).toBeUndefined(); + + await waitFor(() => expect(result.current.loading).toEqual(true)); + }); + + it('should return correct values after loading', async () => { + const { result } = renderHook(() => useAppPluginInstalled('grafana-exploretraces-app')); + + await waitFor(() => expect(result.current.loading).toEqual(false)); + + expect(result.current.loading).toEqual(false); + expect(result.current.error).toBeUndefined(); + expect(result.current.value).toEqual(true); + }); + + it('should return correct values if the pluginId does not exist', async () => { + const { result } = renderHook(() => useAppPluginInstalled('otherorg-otherplugin-app')); + + await waitFor(() => expect(result.current.loading).toEqual(false)); + + expect(result.current.loading).toEqual(false); + expect(result.current.error).toBeUndefined(); + expect(result.current.value).toEqual(false); + }); + + it('should return correct values if isAppPluginInstalled throws', async () => { + isAppPluginInstalledMock.mockRejectedValue(new Error('Some error')); + + const { result } = renderHook(() => useAppPluginInstalled('otherorg-otherplugin-app')); + + await waitFor(() => expect(result.current.loading).toEqual(false)); + + expect(result.current.loading).toEqual(false); + expect(result.current.error).toEqual(new Error('Some error')); + expect(result.current.value).toBeUndefined(); + }); +}); + +describe('useAppPluginVersion', () => { + beforeEach(() => { + setAppPluginMetas(apps); + jest.resetAllMocks(); + getAppPluginVersionMock.mockImplementation(actualApps.getAppPluginVersion); + }); + + it('should return correct default values', async () => { + const { result } = renderHook(() => useAppPluginVersion('grafana-exploretraces-app')); + + expect(result.current.loading).toEqual(true); + expect(result.current.error).toBeUndefined(); + expect(result.current.value).toBeUndefined(); + + await waitFor(() => expect(result.current.loading).toEqual(true)); + }); + + it('should return correct values after loading', async () => { + const { result } = renderHook(() => useAppPluginVersion('grafana-exploretraces-app')); + + await waitFor(() => expect(result.current.loading).toEqual(false)); + + expect(result.current.loading).toEqual(false); + expect(result.current.error).toBeUndefined(); + expect(result.current.value).toEqual('1.2.2'); + }); + + it('should return correct values if the pluginId does not exist', async () => { + const { result } = renderHook(() => useAppPluginVersion('otherorg-otherplugin-app')); + + await waitFor(() => expect(result.current.loading).toEqual(false)); + + expect(result.current.loading).toEqual(false); + expect(result.current.error).toBeUndefined(); + expect(result.current.value).toEqual(null); + }); + + it('should return correct values if getAppPluginVersion throws', async () => { + getAppPluginVersionMock.mockRejectedValue(new Error('Some error')); + + const { result } = renderHook(() => useAppPluginVersion('otherorg-otherplugin-app')); + + await waitFor(() => expect(result.current.loading).toEqual(false)); + + expect(result.current.loading).toEqual(false); + expect(result.current.error).toEqual(new Error('Some error')); + expect(result.current.value).toBeUndefined(); + }); +}); diff --git a/packages/grafana-runtime/src/services/pluginMeta/hooks.tsx b/packages/grafana-runtime/src/services/pluginMeta/hooks.tsx new file mode 100644 index 00000000000..58ac42bbdd2 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/hooks.tsx @@ -0,0 +1,35 @@ +import { useAsync } from 'react-use'; + +import { getAppPluginMeta, getAppPluginMetas, getAppPluginVersion, isAppPluginInstalled } from './apps'; + +export function useAppPluginMetas() { + const { loading, error, value } = useAsync(async () => getAppPluginMetas()); + return { loading, error, value }; +} + +export function useAppPluginMeta(pluginId: string) { + const { loading, error, value } = useAsync(async () => getAppPluginMeta(pluginId)); + return { loading, error, value }; +} + +/** + * Hook that checks if an app plugin is installed. The hook does not check if the app plugin is enabled. + * @param pluginId - The ID of the app plugin. + * @returns loading, error, value of the app plugin installed status. + * The value is true if the app plugin is installed, false otherwise. + */ +export function useAppPluginInstalled(pluginId: string) { + const { loading, error, value } = useAsync(async () => isAppPluginInstalled(pluginId)); + return { loading, error, value }; +} + +/** + * Hook that gets the version of an app plugin. + * @param pluginId - The ID of the app plugin. + * @returns loading, error, value of the app plugin version. + * The value is the version of the app plugin, or null if the plugin is not installed. + */ +export function useAppPluginVersion(pluginId: string) { + const { loading, error, value } = useAsync(async () => getAppPluginVersion(pluginId)); + return { loading, error, value }; +} diff --git a/packages/grafana-runtime/src/services/pluginMeta/mappers/mappers.ts b/packages/grafana-runtime/src/services/pluginMeta/mappers/mappers.ts new file mode 100644 index 00000000000..15505b2edc0 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/mappers/mappers.ts @@ -0,0 +1,7 @@ +import { AppPluginMetasMapper, PluginMetasResponse } from '../types'; + +import { v0alpha1AppMapper } from './v0alpha1AppMapper'; + +export function getAppPluginMapper(): AppPluginMetasMapper { + return v0alpha1AppMapper; +} diff --git a/packages/grafana-runtime/src/services/pluginMeta/mappers/v0alpha1AppMapper.test.ts b/packages/grafana-runtime/src/services/pluginMeta/mappers/v0alpha1AppMapper.test.ts new file mode 100644 index 00000000000..dfc82d41b3e --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/mappers/v0alpha1AppMapper.test.ts @@ -0,0 +1,84 @@ +import { apps } from '../test-fixtures/config.apps'; +import { v0alpha1Response } from '../test-fixtures/v0alpha1Response'; + +import { v0alpha1AppMapper } from './v0alpha1AppMapper'; + +const PLUGIN_IDS = v0alpha1Response.items + .filter((i) => i.spec.pluginJson.type === 'app') + .map((i) => ({ pluginId: i.spec.pluginJson.id })); + +describe('v0alpha1AppMapper', () => { + describe.each(PLUGIN_IDS)('when called for pluginId:$pluginId', ({ pluginId }) => { + it('should map id property correctly', () => { + const result = v0alpha1AppMapper(v0alpha1Response); + + expect(result[pluginId].id).toEqual(apps[pluginId].id); + }); + + it('should map path property correctly', () => { + const result = v0alpha1AppMapper(v0alpha1Response); + + expect(result[pluginId].path).toEqual(apps[pluginId].path); + }); + + it('should map version property correctly', () => { + const result = v0alpha1AppMapper(v0alpha1Response); + + expect(result[pluginId].version).toEqual(apps[pluginId].version); + }); + + it('should map preload property correctly', () => { + const result = v0alpha1AppMapper(v0alpha1Response); + + expect(result[pluginId].preload).toEqual(apps[pluginId].preload); + }); + + it('should map angular property correctly', () => { + const result = v0alpha1AppMapper(v0alpha1Response); + + expect(result[pluginId].angular).toEqual({}); + }); + + it('should map loadingStrategy property correctly', () => { + const result = v0alpha1AppMapper(v0alpha1Response); + + expect(result[pluginId].loadingStrategy).toEqual(apps[pluginId].loadingStrategy); + }); + + it('should map dependencies property correctly', () => { + const result = v0alpha1AppMapper(v0alpha1Response); + + expect(result[pluginId].dependencies).toEqual(apps[pluginId].dependencies); + }); + + it('should map extensions property correctly', () => { + const result = v0alpha1AppMapper(v0alpha1Response); + + expect(result[pluginId].extensions.addedComponents).toEqual(apps[pluginId].extensions.addedComponents); + expect(result[pluginId].extensions.addedFunctions).toEqual(apps[pluginId].extensions.addedFunctions); + expect(result[pluginId].extensions.addedLinks).toEqual(apps[pluginId].extensions.addedLinks); + expect(result[pluginId].extensions.exposedComponents).toEqual(apps[pluginId].extensions.exposedComponents); + expect(result[pluginId].extensions.extensionPoints).toEqual(apps[pluginId].extensions.extensionPoints); + }); + + it('should map moduleHash property correctly', () => { + const result = v0alpha1AppMapper(v0alpha1Response); + + expect(result[pluginId].moduleHash).toEqual(apps[pluginId].moduleHash); + }); + + it('should map buildMode property correctly', () => { + const result = v0alpha1AppMapper(v0alpha1Response); + + expect(result[pluginId].buildMode).toEqual(apps[pluginId].buildMode); + }); + }); + + it('should only map specs with type app', () => { + const result = v0alpha1AppMapper(v0alpha1Response); + + expect(v0alpha1Response.items).toHaveLength(58); + expect(Object.keys(result)).toHaveLength(5); + expect(Object.keys(result)).toEqual(Object.keys(apps)); + }); +}); diff --git a/packages/grafana-runtime/src/services/pluginMeta/mappers/v0alpha1AppMapper.ts b/packages/grafana-runtime/src/services/pluginMeta/mappers/v0alpha1AppMapper.ts new file mode 100644 index 00000000000..aa5ca6e2ce0 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/mappers/v0alpha1AppMapper.ts @@ -0,0 +1,111 @@ +import { + type AngularMeta, + type AppPluginConfig, + type PluginDependencies, + type PluginExtensions, + PluginLoadingStrategy, + type PluginType, +} from '@grafana/data'; + +import type { AppPluginMetas, AppPluginMetasMapper, PluginMetasResponse } from '../types'; +import type { Spec as v0alpha1Spec } from '../types/types.spec.gen'; + +function angularyMapper(spec: v0alpha1Spec): AngularMeta { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return {} as AngularMeta; +} + +function dependenciesMapper(spec: v0alpha1Spec): PluginDependencies { + const plugins = (spec.pluginJson.dependencies?.plugins ?? []).map((v) => ({ + ...v, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + type: v.type as PluginType, + version: '', + })); + + const dependencies: PluginDependencies = { + ...spec.pluginJson.dependencies, + extensions: { + exposedComponents: spec.pluginJson.dependencies.extensions?.exposedComponents ?? [], + }, + grafanaDependency: spec.pluginJson.dependencies.grafanaDependency, + grafanaVersion: spec.pluginJson.dependencies.grafanaVersion ?? '', + plugins, + }; + + return dependencies; +} + +function extensionsMapper(spec: v0alpha1Spec): PluginExtensions { + const addedComponents = spec.pluginJson.extensions?.addedComponents ?? []; + const addedFunctions = spec.pluginJson.extensions?.addedFunctions ?? []; + const addedLinks = spec.pluginJson.extensions?.addedLinks ?? []; + const exposedComponents = (spec.pluginJson.extensions?.exposedComponents ?? []).map((v) => ({ + ...v, + description: v.description ?? '', + title: v.title ?? '', + })); + const extensionPoints = (spec.pluginJson.extensions?.extensionPoints ?? []).map((v) => ({ + ...v, + description: v.description ?? '', + title: v.title ?? '', + })); + + const extensions: PluginExtensions = { + addedComponents, + addedFunctions, + addedLinks, + exposedComponents, + extensionPoints, + }; + + return extensions; +} + +function loadingStrategyMapper(spec: v0alpha1Spec): PluginLoadingStrategy { + const loadingStrategy = spec.module?.loadingStrategy ?? PluginLoadingStrategy.fetch; + if (loadingStrategy === PluginLoadingStrategy.script) { + return PluginLoadingStrategy.script; + } + + return PluginLoadingStrategy.fetch; +} + +function specMapper(spec: v0alpha1Spec): AppPluginConfig { + const { id, info, preload = false } = spec.pluginJson; + const angular = angularyMapper(spec); + const dependencies = dependenciesMapper(spec); + const extensions = extensionsMapper(spec); + const loadingStrategy = loadingStrategyMapper(spec); + const path = spec.module?.path ?? ''; + const version = info.version; + const buildMode = spec.pluginJson.buildMode ?? 'production'; + const moduleHash = spec.module?.hash; + + return { + id, + angular, + dependencies, + extensions, + loadingStrategy, + path, + preload, + version, + buildMode, + moduleHash, + }; +} + +export const v0alpha1AppMapper: AppPluginMetasMapper = (response) => { + const result: AppPluginMetas = {}; + + return response.items.reduce((acc, curr) => { + if (curr.spec.pluginJson.type !== 'app') { + return acc; + } + + const config = specMapper(curr.spec); + acc[config.id] = config; + return acc; + }, result); +}; diff --git a/packages/grafana-runtime/src/services/pluginMeta/plugins.test.ts b/packages/grafana-runtime/src/services/pluginMeta/plugins.test.ts new file mode 100644 index 00000000000..9a5077d1b2b --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/plugins.test.ts @@ -0,0 +1,153 @@ +import { evaluateBooleanFlag } from '../../internal/openFeature'; + +import { clearCache, initPluginMetas } from './plugins'; +import { v0alpha1Meta } from './test-fixtures/v0alpha1Response'; + +jest.mock('../../internal/openFeature', () => ({ + ...jest.requireActual('../../internal/openFeature'), + evaluateBooleanFlag: jest.fn(), +})); + +const evaluateBooleanFlagMock = jest.mocked(evaluateBooleanFlag); + +describe('when useMTPlugins toggle is enabled and cache is not initialized', () => { + const originalFetch = global.fetch; + + beforeEach(() => { + jest.resetAllMocks(); + clearCache(); + evaluateBooleanFlagMock.mockReturnValue(true); + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + it('initPluginMetas should call loadPluginMetas and return correct result if response is ok', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ items: [v0alpha1Meta] }), + }); + + const response = await initPluginMetas(); + + expect(response.items).toHaveLength(1); + expect(response.items[0]).toEqual(v0alpha1Meta); + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith('/apis/plugins.grafana.app/v0alpha1/namespaces/default/metas'); + }); + + it('initPluginMetas should call loadPluginMetas and return correct result if response is not ok', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 404, + statusText: 'Not found', + }); + + await expect(initPluginMetas()).rejects.toThrow(new Error(`Failed to load plugin metas 404:Not found`)); + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith('/apis/plugins.grafana.app/v0alpha1/namespaces/default/metas'); + }); +}); + +describe('when useMTPlugins toggle is enabled and cache is initialized', () => { + const originalFetch = global.fetch; + + beforeEach(() => { + jest.resetAllMocks(); + clearCache(); + evaluateBooleanFlagMock.mockReturnValue(true); + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + it('initPluginMetas should return cache', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ items: [v0alpha1Meta] }), + }); + + const original = await initPluginMetas(); + const cached = await initPluginMetas(); + + expect(original).toEqual(cached); + expect(global.fetch).toHaveBeenCalledTimes(1); + }); + + it('initPluginMetas should return inflight promise', async () => { + jest.useFakeTimers(); + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ items: [v0alpha1Meta] }), + }); + + const original = initPluginMetas(); + const cached = initPluginMetas(); + await jest.runAllTimersAsync(); + + expect(original).toEqual(cached); + expect(global.fetch).toHaveBeenCalledTimes(1); + }); +}); + +describe('when useMTPlugins toggle is disabled and cache is not initialized', () => { + const originalFetch = global.fetch; + + beforeEach(() => { + jest.resetAllMocks(); + clearCache(); + global.fetch = jest.fn(); + evaluateBooleanFlagMock.mockReturnValue(false); + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + it('initPluginMetas should call loadPluginMetas and return correct result if response is ok', async () => { + const response = await initPluginMetas(); + + expect(response.items).toHaveLength(0); + expect(global.fetch).not.toHaveBeenCalled(); + }); +}); + +describe('when useMTPlugins toggle is disabled and cache is initialized', () => { + const originalFetch = global.fetch; + + beforeEach(() => { + jest.resetAllMocks(); + clearCache(); + global.fetch = jest.fn(); + evaluateBooleanFlagMock.mockReturnValue(false); + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + it('initPluginMetas should return cache', async () => { + const original = await initPluginMetas(); + const cached = await initPluginMetas(); + + expect(original).toEqual(cached); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it('initPluginMetas should return inflight promise', async () => { + jest.useFakeTimers(); + + const original = initPluginMetas(); + const cached = initPluginMetas(); + await jest.runAllTimersAsync(); + + expect(original).toEqual(cached); + expect(global.fetch).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/grafana-runtime/src/services/pluginMeta/plugins.ts b/packages/grafana-runtime/src/services/pluginMeta/plugins.ts new file mode 100644 index 00000000000..ec2fa4a9d11 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/plugins.ts @@ -0,0 +1,41 @@ +import { config } from '../../config'; +import { evaluateBooleanFlag } from '../../internal/openFeature'; + +import type { PluginMetasResponse } from './types'; + +let initPromise: Promise | null = null; + +function getApiVersion(): string { + return 'v0alpha1'; +} + +async function loadPluginMetas(): Promise { + if (!evaluateBooleanFlag('useMTPlugins', false)) { + const result = { items: [] }; + return result; + } + + const metas = await fetch(`/apis/plugins.grafana.app/${getApiVersion()}/namespaces/${config.namespace}/metas`); + if (!metas.ok) { + throw new Error(`Failed to load plugin metas ${metas.status}:${metas.statusText}`); + } + + const result = await metas.json(); + return result; +} + +export function initPluginMetas(): Promise { + if (!initPromise) { + initPromise = loadPluginMetas(); + } + + return initPromise; +} + +export function clearCache() { + if (process.env.NODE_ENV !== 'test') { + throw new Error('clearCache() function can only be called from tests.'); + } + + initPromise = null; +} diff --git a/packages/grafana-runtime/src/services/pluginMeta/test-fixtures/config.apps.ts b/packages/grafana-runtime/src/services/pluginMeta/test-fixtures/config.apps.ts new file mode 100644 index 00000000000..365308bd76c --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/test-fixtures/config.apps.ts @@ -0,0 +1,303 @@ +import { cloneDeep } from 'lodash'; + +import { AngularMeta, AppPluginConfig, PluginLoadingStrategy } from '@grafana/data'; + +import { AppPluginMetas } from '../types'; + +export const app: AppPluginConfig = cloneDeep({ + id: 'myorg-someplugin-app', + path: 'public/plugins/myorg-someplugin-app/module.js', + version: '1.0.0', + preload: false, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + angular: { detected: false } as AngularMeta, + loadingStrategy: PluginLoadingStrategy.script, + extensions: { + addedLinks: [], + addedComponents: [], + exposedComponents: [], + extensionPoints: [], + addedFunctions: [], + }, + dependencies: { + grafanaDependency: '>=10.4.0', + grafanaVersion: '*', + plugins: [], + extensions: { + exposedComponents: [], + }, + }, + buildMode: 'production', +}); + +export const apps: AppPluginMetas = cloneDeep({ + 'grafana-exploretraces-app': { + id: 'grafana-exploretraces-app', + path: 'public/plugins/grafana-exploretraces-app/module.js', + version: '1.2.2', + preload: true, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + angular: { detected: false } as AngularMeta, + loadingStrategy: PluginLoadingStrategy.script, + extensions: { + addedLinks: [ + { + targets: ['grafana/dashboard/panel/menu'], + title: 'Open in Traces Drilldown', + description: 'Open current query in the Traces Drilldown app', + }, + { + targets: ['grafana/explore/toolbar/action'], + title: 'Open in Grafana Traces Drilldown', + description: 'Try our new queryless experience for traces', + }, + ], + addedComponents: [ + { + targets: ['grafana-asserts-app/entity-assertions-widget/v1'], + title: 'Asserts widget', + description: 'A block with assertions for a given service', + }, + { + targets: ['grafana-asserts-app/insights-timeline-widget/v1'], + title: 'Insights Timeline Widget', + description: 'Widget for displaying insights timeline in other apps', + }, + ], + exposedComponents: [ + { + id: 'grafana-exploretraces-app/open-in-explore-traces-button/v1', + title: 'Open in Traces Drilldown button', + description: 'A button that opens a traces view in the Traces Drilldown app.', + }, + { + id: 'grafana-exploretraces-app/embedded-trace-exploration/v1', + title: 'Embedded Trace Exploration', + description: + 'A component that renders a trace exploration view that can be embedded in other parts of Grafana.', + }, + ], + extensionPoints: [ + { + id: 'grafana-exploretraces-app/investigation/v1', + title: '', + description: '', + }, + { + id: 'grafana-exploretraces-app/get-logs-drilldown-link/v1', + title: '', + description: '', + }, + ], + addedFunctions: [], + }, + dependencies: { + grafanaDependency: '>=11.5.0', + grafanaVersion: '*', + plugins: [], + extensions: { + exposedComponents: [ + 'grafana-asserts-app/entity-assertions-widget/v1', + 'grafana-asserts-app/insights-timeline-widget/v1', + ], + }, + }, + buildMode: 'production', + }, + 'grafana-lokiexplore-app': { + id: 'grafana-lokiexplore-app', + path: 'public/plugins/grafana-lokiexplore-app/module.js', + version: '1.0.32', + preload: true, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + angular: { detected: false } as AngularMeta, + loadingStrategy: PluginLoadingStrategy.script, + extensions: { + addedLinks: [ + { + targets: [ + 'grafana/dashboard/panel/menu', + 'grafana/explore/toolbar/action', + 'grafana-metricsdrilldown-app/open-in-logs-drilldown/v1', + 'grafana-assistant-app/navigateToDrilldown/v1', + ], + title: 'Open in Grafana Logs Drilldown', + description: 'Open current query in the Grafana Logs Drilldown view', + }, + ], + addedComponents: [ + { + targets: ['grafana-asserts-app/insights-timeline-widget/v1'], + title: 'Insights Timeline Widget', + description: 'Widget for displaying insights timeline in other apps', + }, + ], + exposedComponents: [ + { + id: 'grafana-lokiexplore-app/open-in-explore-logs-button/v1', + title: 'Open in Logs Drilldown button', + description: 'A button that opens a logs view in the Logs Drilldown app.', + }, + { + id: 'grafana-lokiexplore-app/embedded-logs-exploration/v1', + title: 'Embedded Logs Exploration', + description: + 'A component that renders a logs exploration view that can be embedded in other parts of Grafana.', + }, + ], + extensionPoints: [ + { + id: 'grafana-lokiexplore-app/investigation/v1', + title: '', + description: '', + }, + ], + addedFunctions: [ + { + targets: ['grafana-exploretraces-app/get-logs-drilldown-link/v1'], + title: 'Open Logs Drilldown', + description: 'Returns url to logs drilldown app', + }, + ], + }, + dependencies: { + grafanaDependency: '>=11.6.0', + grafanaVersion: '*', + plugins: [], + extensions: { + exposedComponents: [ + 'grafana-adaptivelogs-app/temporary-exemptions/v1', + 'grafana-lokiexplore-app/embedded-logs-exploration/v1', + 'grafana-asserts-app/insights-timeline-widget/v1', + 'grafana/add-to-dashboard-form/v1', + ], + }, + }, + buildMode: 'production', + }, + 'grafana-metricsdrilldown-app': { + id: 'grafana-metricsdrilldown-app', + path: 'public/plugins/grafana-metricsdrilldown-app/module.js', + version: '1.0.26', + preload: true, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + angular: { detected: false } as AngularMeta, + loadingStrategy: PluginLoadingStrategy.script, + extensions: { + addedLinks: [ + { + targets: [ + 'grafana/dashboard/panel/menu', + 'grafana/explore/toolbar/action', + 'grafana-assistant-app/navigateToDrilldown/v1', + 'grafana/alerting/alertingrule/queryeditor', + ], + title: 'Open in Grafana Metrics Drilldown', + description: 'Open current query in the Grafana Metrics Drilldown view', + }, + { + targets: ['grafana-metricsdrilldown-app/grafana-assistant-app/navigateToDrilldown/v0-alpha'], + title: 'Navigate to metrics drilldown', + description: 'Build a url path to the metrics drilldown', + }, + { + targets: ['grafana/datasources/config/actions', 'grafana/datasources/config/status'], + title: 'Open in Metrics Drilldown', + description: 'Browse metrics in Grafana Metrics Drilldown', + }, + ], + addedComponents: [], + exposedComponents: [ + { + id: 'grafana-metricsdrilldown-app/label-breakdown-component/v1', + title: 'Label Breakdown', + description: 'A metrics label breakdown view from the Metrics Drilldown app.', + }, + { + id: 'grafana-metricsdrilldown-app/knowledge-graph-insight-metrics/v1', + title: 'Knowledge Graph Source Metrics', + description: 'Explore the underlying metrics related to a Knowledge Graph insight', + }, + ], + extensionPoints: [ + { + id: 'grafana-exploremetrics-app/investigation/v1', + title: '', + description: '', + }, + { + id: 'grafana-metricsdrilldown-app/open-in-logs-drilldown/v1', + title: '', + description: '', + }, + ], + addedFunctions: [], + }, + dependencies: { + grafanaDependency: '>=11.6.0', + grafanaVersion: '*', + plugins: [], + extensions: { + exposedComponents: ['grafana/add-to-dashboard-form/v1'], + }, + }, + buildMode: 'production', + }, + 'grafana-pyroscope-app': { + id: 'grafana-pyroscope-app', + path: 'public/plugins/grafana-pyroscope-app/module.js', + version: '1.14.2', + preload: true, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + angular: { detected: false } as AngularMeta, + loadingStrategy: PluginLoadingStrategy.script, + extensions: { + addedLinks: [ + { + targets: [ + 'grafana/explore/toolbar/action', + 'grafana/traceview/details', + 'grafana-assistant-app/navigateToDrilldown/v1', + ], + title: 'Open in Grafana Profiles Drilldown', + description: 'Try our new queryless experience for profiles', + }, + ], + addedComponents: [], + exposedComponents: [ + { + id: 'grafana-pyroscope-app/embedded-profiles-exploration/v1', + title: 'Embedded Profiles Exploration', + description: + 'A component that renders a profiles exploration view that can be embedded in other parts of Grafana.', + }, + ], + extensionPoints: [ + { + id: 'grafana-pyroscope-app/investigation/v1', + title: '', + description: '', + }, + { + id: 'grafana-pyroscope-app/settings/v1', + title: '', + description: '', + }, + ], + addedFunctions: [], + }, + dependencies: { + grafanaDependency: '>=11.5.0', + grafanaVersion: '*', + plugins: [], + extensions: { + exposedComponents: [ + 'grafana-o11yinsights-app/insights-launcher/v1', + 'grafana-adaptiveprofiles-app/resolution-boost/v1', + ], + }, + }, + buildMode: 'production', + }, + [app.id]: app, +}); diff --git a/packages/grafana-runtime/src/services/pluginMeta/test-fixtures/v0alpha1Response.ts b/packages/grafana-runtime/src/services/pluginMeta/test-fixtures/v0alpha1Response.ts new file mode 100644 index 00000000000..7bd4c38d9fa --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/test-fixtures/v0alpha1Response.ts @@ -0,0 +1,4378 @@ +import { cloneDeep } from 'lodash'; + +import type { PluginMetasResponse } from '../types'; +import type { Meta } from '../types/meta_object_gen'; + +export const v0alpha1Meta: Meta = cloneDeep({ + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'myorg-someplugin-app', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'myorg-someplugin-app', + type: 'app', + name: 'Some-Plugin', + info: { + keywords: ['app'], + logos: { + small: 'public/plugins/myorg-someplugin-app/img/logo.svg', + large: 'public/plugins/myorg-someplugin-app/img/logo.svg', + }, + updated: '2025-12-15', + version: '1.0.0', + author: { + name: 'Myorg', + }, + }, + dependencies: { + grafanaDependency: '>=10.4.0', + grafanaVersion: '*', + }, + includes: [ + { + type: 'page', + name: 'Page One', + role: 'Viewer', + action: 'plugins.app:access', + path: '/a/myorg-someplugin-app/one', + addToNav: true, + defaultNav: true, + }, + { + type: 'page', + name: 'Page Two', + role: 'Viewer', + action: 'plugins.app:access', + path: '/a/myorg-someplugin-app/two', + addToNav: true, + }, + { + type: 'page', + name: 'Page Three', + role: 'Viewer', + action: 'plugins.app:access', + path: '/a/myorg-someplugin-app/three', + addToNav: true, + }, + { + type: 'page', + name: 'Page Four', + role: 'Viewer', + action: 'plugins.app:access', + path: '/a/myorg-someplugin-app/four', + addToNav: true, + }, + { + type: 'page', + name: 'Configuration', + role: 'Admin', + path: '/plugins/myorg-someplugin-app', + addToNav: true, + icon: 'cog', + }, + ], + }, + class: 'external', + module: { + path: 'public/plugins/myorg-someplugin-app/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/myorg-someplugin-app', + signature: { + status: 'unsigned', + }, + angular: { + detected: false, + }, + }, + status: {}, +}); + +export const v0alpha1Response: PluginMetasResponse = cloneDeep({ + items: [ + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'alertlist', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'alertlist', + type: 'panel', + name: 'Alert list', + info: { + keywords: [], + logos: { + small: 'public/plugins/alertlist/img/icn-singlestat-panel.svg', + large: 'public/plugins/alertlist/img/icn-singlestat-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Shows list of alerts and their current status', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/alert-list/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + skipDataQuery: true, + }, + class: 'core', + module: { + path: 'core:plugin/alertlist', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/alertlist', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'alertmanager', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'alertmanager', + type: 'datasource', + name: 'Alertmanager', + info: { + keywords: ['alerts', 'alerting', 'prometheus', 'alertmanager', 'mimir', 'cortex'], + logos: { + small: 'public/plugins/alertmanager/img/logo.svg', + large: 'public/plugins/alertmanager/img/logo.svg', + }, + updated: '', + version: '', + author: { + name: 'Prometheus alertmanager', + url: 'https://grafana.com', + }, + description: + 'Add external Alertmanagers (supports Prometheus and Mimir implementations) so you can use the Grafana Alerting UI to manage silences, contact points, and notification policies.', + links: [ + { + name: 'Learn more', + url: 'https://prometheus.io/docs/alerting/latest/alertmanager/', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/alertmanager/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + routes: [ + { + path: 'alertmanager/api/v2/silences', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'api/v2/silences', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'alertmanager/api/v2/silences', + method: 'POST', + reqRole: 'Editor', + reqAction: 'alert.instances.external:write', + }, + { + path: 'api/v2/silences', + method: 'POST', + reqRole: 'Editor', + reqAction: 'alert.instances.external:write', + }, + { + path: 'alertmanager/api/v2/silence/', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'api/v2/silence/', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'alertmanager/api/v2/silence/', + method: 'DELETE', + reqRole: 'Editor', + reqAction: 'alert.instances.external:write', + }, + { + path: 'api/v2/silence/', + method: 'DELETE', + reqRole: 'Editor', + reqAction: 'alert.instances.external:write', + }, + { + path: 'alertmanager/api/v2/alerts/groups', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'api/v2/alerts/groups', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'alertmanager/api/v2/alerts', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'api/v2/alerts', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'alertmanager/api/v2/alerts', + method: 'POST', + reqRole: 'Editor', + reqAction: 'alert.instances.external:write', + }, + { + path: 'api/v2/alerts', + method: 'POST', + reqRole: 'Editor', + reqAction: 'alert.instances.external:write', + }, + { + path: 'alertmanager/api/v2/status', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.notifications.external:read', + }, + { + path: 'api/v2/status', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.notifications.external:read', + }, + { + path: 'alertmanager/api/v2/receivers', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'api/v2/receivers', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.instances.external:read', + }, + { + path: 'api/v1/alerts', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.notifications.external:read', + }, + { + path: 'api/v1/alerts', + method: 'POST', + reqRole: 'Editor', + reqAction: 'alert.notifications.external:write', + }, + { + path: 'api/v1/alerts', + method: 'DELETE', + reqRole: 'Editor', + reqAction: 'alert.notifications.external:write', + }, + { + method: 'POST', + reqRole: 'Admin', + }, + { + method: 'PUT', + reqRole: 'Admin', + }, + { + method: 'DELETE', + reqRole: 'Admin', + }, + { + method: 'GET', + reqRole: 'Admin', + }, + ], + }, + class: 'core', + module: { + path: 'core:plugin/alertmanager', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/alertmanager', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'annolist', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'annolist', + type: 'panel', + name: 'Annotations list', + info: { + keywords: [], + logos: { + small: 'public/plugins/annolist/img/icn-annolist-panel.svg', + large: 'public/plugins/annolist/img/icn-annolist-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'List annotations', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/annotations/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + skipDataQuery: true, + }, + class: 'core', + module: { + path: 'core:plugin/annolist', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/annolist', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'barchart', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'barchart', + type: 'panel', + name: 'Bar chart', + info: { + keywords: [], + logos: { + small: 'public/plugins/barchart/img/barchart.svg', + large: 'public/plugins/barchart/img/barchart.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Categorical charts with group support', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/bar-chart/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/barchart', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/barchart', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'bargauge', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'bargauge', + type: 'panel', + name: 'Bar gauge', + info: { + keywords: [], + logos: { + small: 'public/plugins/bargauge/img/icon_bar_gauge.svg', + large: 'public/plugins/bargauge/img/icon_bar_gauge.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Horizontal and vertical gauges', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/bar-gauge/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/bargauge', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/bargauge', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'candlestick', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'candlestick', + type: 'panel', + name: 'Candlestick', + info: { + keywords: ['financial', 'price', 'currency', 'k-line'], + logos: { + small: 'public/plugins/candlestick/img/candlestick.svg', + large: 'public/plugins/candlestick/img/candlestick.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Graphical representation of price movements of a security, derivative, or currency.', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/candlestick/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/candlestick', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/candlestick', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'canvas', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'canvas', + type: 'panel', + name: 'Canvas', + info: { + keywords: [], + logos: { + small: 'public/plugins/canvas/img/icn-canvas.svg', + large: 'public/plugins/canvas/img/icn-canvas.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Explicit element placement', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/canvas/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/canvas', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/canvas', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'cloudwatch', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'cloudwatch', + type: 'datasource', + name: 'CloudWatch', + info: { + keywords: ['aws', 'amazon'], + logos: { + small: 'public/plugins/cloudwatch/img/amazon-web-services.png', + large: 'public/plugins/cloudwatch/img/amazon-web-services.png', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Data source for Amazon AWS monitoring service', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'cloud', + includes: [ + { + type: 'dashboard', + name: 'EC2', + role: 'Viewer', + path: 'dashboards/ec2.json', + }, + { + type: 'dashboard', + name: 'EBS', + role: 'Viewer', + path: 'dashboards/EBS.json', + }, + { + type: 'dashboard', + name: 'Lambda', + role: 'Viewer', + path: 'dashboards/Lambda.json', + }, + { + type: 'dashboard', + name: 'Logs', + role: 'Viewer', + path: 'dashboards/Logs.json', + }, + { + type: 'dashboard', + name: 'RDS', + role: 'Viewer', + path: 'dashboards/RDS.json', + }, + ], + logs: true, + metrics: true, + queryOptions: { + minInterval: true, + }, + }, + class: 'core', + module: { + path: 'core:plugin/cloudwatch', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/cloudwatch', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'dashboard', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'dashboard', + type: 'datasource', + name: '-- Dashboard --', + info: { + keywords: [], + logos: { + small: 'public/plugins/dashboard/img/icn-reusequeries.svg', + large: 'public/plugins/dashboard/img/icn-reusequeries.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Uses the result set from another panel in the same dashboard', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + builtIn: true, + metrics: true, + }, + class: 'core', + module: { + path: 'core:plugin/dashboard', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/dashboard', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'dashlist', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'dashlist', + type: 'panel', + name: 'Dashboard list', + info: { + keywords: [], + logos: { + small: 'public/plugins/dashlist/img/icn-dashlist-panel.svg', + large: 'public/plugins/dashlist/img/icn-dashlist-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'List of dynamic links to other dashboards', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/dashboard-list/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + skipDataQuery: true, + }, + class: 'core', + module: { + path: 'core:plugin/dashlist', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/dashlist', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'datagrid', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'datagrid', + type: 'panel', + name: 'Datagrid', + info: { + keywords: [], + logos: { + small: 'public/plugins/datagrid/img/icn-table-panel.svg', + large: 'public/plugins/datagrid/img/icn-table-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/datagrid/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + state: 'beta', + }, + class: 'core', + module: { + path: 'core:plugin/datagrid', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/datagrid', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'debug', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'debug', + type: 'panel', + name: 'Debug', + info: { + keywords: [], + logos: { + small: 'public/plugins/debug/img/icn-debug.svg', + large: 'public/plugins/debug/img/icn-debug.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Debug Panel for Grafana', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + state: 'alpha', + }, + class: 'core', + module: { + path: 'core:plugin/debug', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/debug', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'elasticsearch', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'elasticsearch', + type: 'datasource', + name: 'Elasticsearch', + info: { + keywords: ['elasticsearch', 'datasource', 'database', 'logs', 'nosql', 'traces'], + logos: { + small: 'public/plugins/elasticsearch/img/elasticsearch.svg', + large: 'public/plugins/elasticsearch/img/elasticsearch.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Open source logging & analytics database', + links: [ + { + name: 'Learn more', + url: 'https://grafana.com/docs/features/datasources/elasticsearch/', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/elasticsearch/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'logging', + logs: true, + metrics: true, + queryOptions: { + minInterval: true, + }, + }, + class: 'core', + module: { + path: 'core:plugin/elasticsearch', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/elasticsearch', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'flamegraph', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'flamegraph', + type: 'panel', + name: 'Flame Graph', + info: { + keywords: [], + logos: { + small: 'public/plugins/flamegraph/img/icn-flamegraph.svg', + large: 'public/plugins/flamegraph/img/icn-flamegraph.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/flame-graph/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/flamegraph', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/flamegraph', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'gauge', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'gauge', + type: 'panel', + name: 'Gauge', + info: { + keywords: [], + logos: { + small: 'public/plugins/gauge/img/icon_gauge.svg', + large: 'public/plugins/gauge/img/icon_gauge.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Standard gauge visualization', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/gauge/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/gauge', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/gauge', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'geomap', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'geomap', + type: 'panel', + name: 'Geomap', + info: { + keywords: [], + logos: { + small: 'public/plugins/geomap/img/icn-geomap.svg', + large: 'public/plugins/geomap/img/icn-geomap.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Geomap panel', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/geomap/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/geomap', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/geomap', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'gettingstarted', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'gettingstarted', + type: 'panel', + name: 'Getting Started', + info: { + keywords: [], + logos: { + small: 'public/plugins/gettingstarted/img/icn-dashlist-panel.svg', + large: 'public/plugins/gettingstarted/img/icn-dashlist-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + hideFromList: true, + skipDataQuery: true, + }, + class: 'core', + module: { + path: 'core:plugin/gettingstarted', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/gettingstarted', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana', + type: 'datasource', + name: '-- Grafana --', + info: { + keywords: [], + logos: { + small: 'public/plugins/grafana/img/icn-grafanadb.svg', + large: 'public/plugins/grafana/img/icn-grafanadb.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: + 'A built-in data source that generates random walk data and can poll the Testdata data source. This helps you test visualizations and run experiments.', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + annotations: true, + backend: true, + builtIn: true, + metrics: true, + }, + class: 'core', + module: { + path: 'core:plugin/grafana', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana-azure-monitor-datasource', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana-azure-monitor-datasource', + type: 'datasource', + name: 'Azure Monitor', + info: { + keywords: ['azure', 'monitor', 'Application Insights', 'Log Analytics', 'App Insights'], + logos: { + small: 'public/plugins/grafana-azure-monitor-datasource/img/logo.jpg', + large: 'public/plugins/grafana-azure-monitor-datasource/img/logo.jpg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Data source for Microsoft Azure Monitor & Application Insights', + links: [ + { + name: 'Learn more', + url: 'https://grafana.com/docs/grafana/latest/datasources/azuremonitor/', + }, + { + name: 'License', + url: 'https://github.com/grafana/grafana/blob/HEAD/LICENSE', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/azure-monitor/', + }, + ], + screenshots: [ + { + name: 'Azure Contoso Loans', + path: 'public/plugins/grafana-azure-monitor-datasource/img/contoso_loans_grafana_dashboard.png', + }, + { + name: 'Azure Monitor Network', + path: 'public/plugins/grafana-azure-monitor-datasource/img/azure_monitor_network.png', + }, + { + name: 'Azure Monitor CPU', + path: 'public/plugins/grafana-azure-monitor-datasource/img/azure_monitor_cpu.png', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'cloud', + executable: 'gpx_azuremonitor', + includes: [ + { + type: 'dashboard', + name: 'Azure / Alert Consumption', + role: 'Viewer', + path: 'dashboards/v1Alerts.json', + }, + { + type: 'dashboard', + name: 'Azure / Infrastructure / Apps Monitoring', + role: 'Viewer', + path: 'dashboards/azureInfraApps.json', + }, + { + type: 'dashboard', + name: 'Azure / Infrastructure / Compute Monitoring', + role: 'Viewer', + path: 'dashboards/azureInfraCompute.json', + }, + { + type: 'dashboard', + name: 'Azure / Infrastructure / Data Monitoring', + role: 'Viewer', + path: 'dashboards/azureInfraData.json', + }, + { + type: 'dashboard', + name: 'Azure / Infrastructure / Network Monitoring', + role: 'Viewer', + path: 'dashboards/azureInfraNetwork.json', + }, + { + type: 'dashboard', + name: 'Azure / Infrastructure / Storage and Key Vaults Monitoring', + role: 'Viewer', + path: 'dashboards/azureInfraStorageVaults.json', + }, + { + type: 'dashboard', + name: 'Azure / Azure PostgreSQL / Flexible Server Monitoring', + role: 'Viewer', + path: 'dashboards/postgresFlexibleServer.json', + }, + { + type: 'dashboard', + name: 'Azure Monitor / Container Insights / Syslog', + role: 'Viewer', + path: 'dashboards/containerInsightsSyslog.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Applications', + role: 'Viewer', + path: 'dashboards/appInsights.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Applications / Performance / Operations', + role: 'Viewer', + path: 'dashboards/appInsightsPerfOperations.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Applications / Performance / Dependencies', + role: 'Viewer', + path: 'dashboards/appInsightsPerfDependencies.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Applications / Failures / Operations', + role: 'Viewer', + path: 'dashboards/appInsightsFailureOperations.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Applications / Failures / Dependencies', + role: 'Viewer', + path: 'dashboards/appInsightsFailureDependencies.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Applications / Failures / Exceptions', + role: 'Viewer', + path: 'dashboards/appInsightsFailureExceptions.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Applications Test Availability Geo Map', + role: 'Viewer', + path: 'dashboards/appInsightsGeoMap.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / CosmosDB', + role: 'Viewer', + path: 'dashboards/cosmosdb.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Data Explorer Clusters', + role: 'Viewer', + path: 'dashboards/adx.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Key Vaults', + role: 'Viewer', + path: 'dashboards/keyvault.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Networks', + role: 'Viewer', + path: 'dashboards/networkInsightsDashboard.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / SQL Database', + role: 'Viewer', + path: 'dashboards/sqldb.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Storage Accounts', + role: 'Viewer', + path: 'dashboards/storage.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Virtual Machines by Resource Group', + role: 'Viewer', + path: 'dashboards/vMInsightsRG.json', + }, + { + type: 'dashboard', + name: 'Azure / Insights / Virtual Machines by Workspace', + role: 'Viewer', + path: 'dashboards/vMInsightsWorkspace.json', + }, + { + type: 'dashboard', + name: 'Azure / Resources Overview', + role: 'Viewer', + path: 'dashboards/arg.json', + }, + ], + logs: true, + metrics: true, + tracing: true, + }, + class: 'core', + module: { + path: 'public/plugins/grafana-azure-monitor-datasource/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana-azure-monitor-datasource', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + translations: { + 'cs-CZ': + 'public/plugins/grafana-azure-monitor-datasource/locales/cs-CZ/grafana-azure-monitor-datasource.json', + 'de-DE': + 'public/plugins/grafana-azure-monitor-datasource/locales/de-DE/grafana-azure-monitor-datasource.json', + 'en-US': + 'public/plugins/grafana-azure-monitor-datasource/locales/en-US/grafana-azure-monitor-datasource.json', + 'es-ES': + 'public/plugins/grafana-azure-monitor-datasource/locales/es-ES/grafana-azure-monitor-datasource.json', + 'fr-FR': + 'public/plugins/grafana-azure-monitor-datasource/locales/fr-FR/grafana-azure-monitor-datasource.json', + 'hu-HU': + 'public/plugins/grafana-azure-monitor-datasource/locales/hu-HU/grafana-azure-monitor-datasource.json', + 'id-ID': + 'public/plugins/grafana-azure-monitor-datasource/locales/id-ID/grafana-azure-monitor-datasource.json', + 'it-IT': + 'public/plugins/grafana-azure-monitor-datasource/locales/it-IT/grafana-azure-monitor-datasource.json', + 'ja-JP': + 'public/plugins/grafana-azure-monitor-datasource/locales/ja-JP/grafana-azure-monitor-datasource.json', + 'ko-KR': + 'public/plugins/grafana-azure-monitor-datasource/locales/ko-KR/grafana-azure-monitor-datasource.json', + 'nl-NL': + 'public/plugins/grafana-azure-monitor-datasource/locales/nl-NL/grafana-azure-monitor-datasource.json', + 'pl-PL': + 'public/plugins/grafana-azure-monitor-datasource/locales/pl-PL/grafana-azure-monitor-datasource.json', + 'pt-BR': + 'public/plugins/grafana-azure-monitor-datasource/locales/pt-BR/grafana-azure-monitor-datasource.json', + 'pt-PT': + 'public/plugins/grafana-azure-monitor-datasource/locales/pt-PT/grafana-azure-monitor-datasource.json', + 'ru-RU': + 'public/plugins/grafana-azure-monitor-datasource/locales/ru-RU/grafana-azure-monitor-datasource.json', + 'sv-SE': + 'public/plugins/grafana-azure-monitor-datasource/locales/sv-SE/grafana-azure-monitor-datasource.json', + 'tr-TR': + 'public/plugins/grafana-azure-monitor-datasource/locales/tr-TR/grafana-azure-monitor-datasource.json', + 'zh-Hans': + 'public/plugins/grafana-azure-monitor-datasource/locales/zh-Hans/grafana-azure-monitor-datasource.json', + 'zh-Hant': + 'public/plugins/grafana-azure-monitor-datasource/locales/zh-Hant/grafana-azure-monitor-datasource.json', + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana-exploretraces-app', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana-exploretraces-app', + type: 'app', + name: 'Grafana Traces Drilldown', + info: { + keywords: ['app', 'tempo', 'traces', 'explore'], + logos: { + small: 'public/plugins/grafana-exploretraces-app/img/logo.svg', + large: 'public/plugins/grafana-exploretraces-app/img/logo.svg', + }, + updated: '2025-12-04', + version: '1.2.2', + author: { + name: 'Grafana', + }, + description: + 'Use Rate, Errors, and Duration (RED) metrics derived from traces to investigate errors within complex distributed systems.', + links: [ + { + name: 'Github', + url: 'https://github.com/grafana/explore-traces', + }, + { + name: 'Report bug', + url: 'https://github.com/grafana/explore-traces/issues/new', + }, + ], + screenshots: [ + { + name: 'histogram-breakdown', + path: 'public/plugins/grafana-exploretraces-app/img/histogram-breakdown.png', + }, + { + name: 'errors-metric-flow', + path: 'public/plugins/grafana-exploretraces-app/img/errors-metric-flow.png', + }, + { + name: 'errors-root-cause', + path: 'public/plugins/grafana-exploretraces-app/img/errors-root-cause.png', + }, + ], + }, + dependencies: { + grafanaDependency: '>=11.5.0', + grafanaVersion: '*', + extensions: { + exposedComponents: [ + 'grafana-asserts-app/entity-assertions-widget/v1', + 'grafana-asserts-app/insights-timeline-widget/v1', + ], + }, + }, + autoEnabled: true, + includes: [ + { + type: 'page', + name: 'Explore', + role: 'Viewer', + action: 'datasources:explore', + path: '/a/grafana-exploretraces-app/', + addToNav: true, + defaultNav: true, + }, + ], + preload: true, + extensions: { + addedComponents: [ + { + targets: ['grafana-asserts-app/entity-assertions-widget/v1'], + title: 'Asserts widget', + description: 'A block with assertions for a given service', + }, + { + targets: ['grafana-asserts-app/insights-timeline-widget/v1'], + title: 'Insights Timeline Widget', + description: 'Widget for displaying insights timeline in other apps', + }, + ], + addedLinks: [ + { + targets: ['grafana/dashboard/panel/menu'], + title: 'Open in Traces Drilldown', + description: 'Open current query in the Traces Drilldown app', + }, + { + targets: ['grafana/explore/toolbar/action'], + title: 'Open in Grafana Traces Drilldown', + description: 'Try our new queryless experience for traces', + }, + ], + exposedComponents: [ + { + id: 'grafana-exploretraces-app/open-in-explore-traces-button/v1', + title: 'Open in Traces Drilldown button', + description: 'A button that opens a traces view in the Traces Drilldown app.', + }, + { + id: 'grafana-exploretraces-app/embedded-trace-exploration/v1', + title: 'Embedded Trace Exploration', + description: + 'A component that renders a trace exploration view that can be embedded in other parts of Grafana.', + }, + ], + extensionPoints: [ + { + id: 'grafana-exploretraces-app/investigation/v1', + }, + { + id: 'grafana-exploretraces-app/get-logs-drilldown-link/v1', + }, + ], + }, + }, + class: 'external', + module: { + path: 'public/plugins/grafana-exploretraces-app/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana-exploretraces-app', + signature: { + status: 'valid', + type: 'grafana', + org: 'Grafana Labs', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana-lokiexplore-app', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana-lokiexplore-app', + type: 'app', + name: 'Grafana Logs Drilldown', + info: { + keywords: ['app', 'loki', 'explore', 'logs', 'drilldown', 'drill', 'down', 'drill-down'], + logos: { + small: 'public/plugins/grafana-lokiexplore-app/img/logo.svg', + large: 'public/plugins/grafana-lokiexplore-app/img/logo.svg', + }, + updated: '2025-12-09', + version: '1.0.32', + author: { + name: 'Grafana', + }, + description: + 'Visualize log volumes to easily detect anomalies or significant changes over time, without needing to compose LogQL queries.', + links: [ + { + name: 'Github', + url: 'https://github.com/grafana/explore-logs', + }, + { + name: 'Report bug', + url: 'https://github.com/grafana/explore-logs/issues/new', + }, + ], + screenshots: [ + { + name: 'patterns', + path: 'public/plugins/grafana-lokiexplore-app/img/patterns.png', + }, + { + name: 'fields', + path: 'public/plugins/grafana-lokiexplore-app/img/fields.png', + }, + { + name: 'table', + path: 'public/plugins/grafana-lokiexplore-app/img/table.png', + }, + ], + }, + dependencies: { + grafanaDependency: '>=11.6.0', + grafanaVersion: '*', + extensions: { + exposedComponents: [ + 'grafana-adaptivelogs-app/temporary-exemptions/v1', + 'grafana-lokiexplore-app/embedded-logs-exploration/v1', + 'grafana-asserts-app/insights-timeline-widget/v1', + 'grafana/add-to-dashboard-form/v1', + ], + }, + }, + autoEnabled: true, + includes: [ + { + type: 'page', + name: 'Grafana Logs Drilldown', + role: 'Viewer', + action: 'datasources:explore', + path: '/a/grafana-lokiexplore-app/explore', + addToNav: true, + defaultNav: true, + }, + ], + preload: true, + extensions: { + addedComponents: [ + { + targets: ['grafana-asserts-app/insights-timeline-widget/v1'], + title: 'Insights Timeline Widget', + description: 'Widget for displaying insights timeline in other apps', + }, + ], + addedLinks: [ + { + targets: [ + 'grafana/dashboard/panel/menu', + 'grafana/explore/toolbar/action', + 'grafana-metricsdrilldown-app/open-in-logs-drilldown/v1', + 'grafana-assistant-app/navigateToDrilldown/v1', + ], + title: 'Open in Grafana Logs Drilldown', + description: 'Open current query in the Grafana Logs Drilldown view', + }, + ], + addedFunctions: [ + { + targets: ['grafana-exploretraces-app/get-logs-drilldown-link/v1'], + title: 'Open Logs Drilldown', + description: 'Returns url to logs drilldown app', + }, + ], + exposedComponents: [ + { + id: 'grafana-lokiexplore-app/open-in-explore-logs-button/v1', + title: 'Open in Logs Drilldown button', + description: 'A button that opens a logs view in the Logs Drilldown app.', + }, + { + id: 'grafana-lokiexplore-app/embedded-logs-exploration/v1', + title: 'Embedded Logs Exploration', + description: + 'A component that renders a logs exploration view that can be embedded in other parts of Grafana.', + }, + ], + extensionPoints: [ + { + id: 'grafana-lokiexplore-app/investigation/v1', + }, + ], + }, + }, + class: 'external', + module: { + path: 'public/plugins/grafana-lokiexplore-app/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana-lokiexplore-app', + signature: { + status: 'valid', + type: 'grafana', + org: 'Grafana Labs', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana-metricsdrilldown-app', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana-metricsdrilldown-app', + type: 'app', + name: 'Grafana Metrics Drilldown', + info: { + keywords: ['drilldown', 'metrics', 'app', 'prometheus', 'mimir'], + logos: { + small: 'public/plugins/grafana-metricsdrilldown-app/img/logo.svg', + large: 'public/plugins/grafana-metricsdrilldown-app/img/logo.svg', + }, + updated: '2025-12-17', + version: '1.0.26', + author: { + name: 'Grafana', + }, + description: + 'Quickly find related metrics with a few clicks, without needing to write PromQL queries to retrieve metrics.', + links: [ + { + name: 'GitHub', + url: 'https://github.com/grafana/metrics-drilldown', + }, + { + name: 'Report a bug', + url: 'https://github.com/grafana/metrics-drilldown/issues/new', + }, + ], + screenshots: [ + { + name: 'metricselect', + path: 'public/plugins/grafana-metricsdrilldown-app/img/metrics-drilldown.png', + }, + { + name: 'breakdown', + path: 'public/plugins/grafana-metricsdrilldown-app/img/breakdown.png', + }, + ], + }, + dependencies: { + grafanaDependency: '>=11.6.0', + grafanaVersion: '*', + extensions: { + exposedComponents: ['grafana/add-to-dashboard-form/v1'], + }, + }, + autoEnabled: true, + includes: [ + { + type: 'page', + name: 'Grafana Metrics Drilldown', + role: 'Viewer', + action: 'datasources:explore', + path: '/a/grafana-metricsdrilldown-app/drilldown', + addToNav: true, + defaultNav: true, + }, + ], + preload: true, + extensions: { + addedLinks: [ + { + targets: [ + 'grafana/dashboard/panel/menu', + 'grafana/explore/toolbar/action', + 'grafana-assistant-app/navigateToDrilldown/v1', + 'grafana/alerting/alertingrule/queryeditor', + ], + title: 'Open in Grafana Metrics Drilldown', + description: 'Open current query in the Grafana Metrics Drilldown view', + }, + { + targets: ['grafana-metricsdrilldown-app/grafana-assistant-app/navigateToDrilldown/v0-alpha'], + title: 'Navigate to metrics drilldown', + description: 'Build a url path to the metrics drilldown', + }, + { + targets: ['grafana/datasources/config/actions', 'grafana/datasources/config/status'], + title: 'Open in Metrics Drilldown', + description: 'Browse metrics in Grafana Metrics Drilldown', + }, + ], + exposedComponents: [ + { + id: 'grafana-metricsdrilldown-app/label-breakdown-component/v1', + title: 'Label Breakdown', + description: 'A metrics label breakdown view from the Metrics Drilldown app.', + }, + { + id: 'grafana-metricsdrilldown-app/knowledge-graph-insight-metrics/v1', + title: 'Knowledge Graph Source Metrics', + description: 'Explore the underlying metrics related to a Knowledge Graph insight', + }, + ], + extensionPoints: [ + { + id: 'grafana-exploremetrics-app/investigation/v1', + }, + { + id: 'grafana-metricsdrilldown-app/open-in-logs-drilldown/v1', + }, + ], + }, + }, + class: 'external', + module: { + path: 'public/plugins/grafana-metricsdrilldown-app/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana-metricsdrilldown-app', + signature: { + status: 'valid', + type: 'grafana', + org: 'Grafana Labs', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana-postgresql-datasource', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana-postgresql-datasource', + type: 'datasource', + name: 'PostgreSQL', + info: { + keywords: [], + logos: { + small: 'public/plugins/grafana-postgresql-datasource/img/postgresql_logo.svg', + large: 'public/plugins/grafana-postgresql-datasource/img/postgresql_logo.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Data source for PostgreSQL and compatible databases', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/postgres/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=11.6.0', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'sql', + executable: 'gpx_grafana-postgresql-datasource', + logs: true, + metrics: true, + queryOptions: { + minInterval: true, + }, + }, + class: 'core', + module: { + path: 'public/plugins/grafana-postgresql-datasource/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana-postgresql-datasource', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana-pyroscope-app', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana-pyroscope-app', + type: 'app', + name: 'Grafana Profiles Drilldown', + info: { + keywords: ['app', 'pyroscope', 'profiling', 'explore', 'profiles', 'performance', 'drilldown'], + logos: { + small: 'public/plugins/grafana-pyroscope-app/img/logo.svg', + large: 'public/plugins/grafana-pyroscope-app/img/logo.svg', + }, + updated: '2025-12-18', + version: '1.14.2', + author: { + name: 'Grafana', + }, + description: + 'View and analyze high-level service performance, identify problem processes for optimization, and diagnose issues to determine root causes.', + links: [ + { + name: 'GitHub', + url: 'https://github.com/grafana/profiles-drilldown', + }, + { + name: 'Report bug', + url: 'https://github.com/grafana/profiles-drilldown/issues/new', + }, + ], + screenshots: [ + { + name: 'Hero Image', + path: 'public/plugins/grafana-pyroscope-app/img/hero-image.png', + }, + ], + }, + dependencies: { + grafanaDependency: '>=11.5.0', + grafanaVersion: '*', + extensions: { + exposedComponents: [ + 'grafana-o11yinsights-app/insights-launcher/v1', + 'grafana-adaptiveprofiles-app/resolution-boost/v1', + ], + }, + }, + autoEnabled: true, + includes: [ + { + type: 'page', + name: 'Profiles', + role: 'Viewer', + action: 'datasources:explore', + path: '/a/grafana-pyroscope-app/explore', + addToNav: true, + defaultNav: true, + }, + ], + preload: true, + extensions: { + addedLinks: [ + { + targets: [ + 'grafana/explore/toolbar/action', + 'grafana/traceview/details', + 'grafana-assistant-app/navigateToDrilldown/v1', + ], + title: 'Open in Grafana Profiles Drilldown', + description: 'Try our new queryless experience for profiles', + }, + ], + exposedComponents: [ + { + id: 'grafana-pyroscope-app/embedded-profiles-exploration/v1', + title: 'Embedded Profiles Exploration', + description: + 'A component that renders a profiles exploration view that can be embedded in other parts of Grafana.', + }, + ], + extensionPoints: [ + { + id: 'grafana-pyroscope-app/investigation/v1', + }, + { + id: 'grafana-pyroscope-app/settings/v1', + }, + ], + }, + }, + class: 'external', + module: { + path: 'public/plugins/grafana-pyroscope-app/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana-pyroscope-app', + signature: { + status: 'valid', + type: 'grafana', + org: 'Grafana Labs', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana-pyroscope-datasource', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana-pyroscope-datasource', + type: 'datasource', + name: 'Grafana Pyroscope', + info: { + keywords: [ + 'grafana', + 'datasource', + 'phlare', + 'flamegraph', + 'profiling', + 'continuous profiling', + 'pyroscope', + ], + logos: { + small: 'public/plugins/grafana-pyroscope-datasource/img/grafana_pyroscope_icon.svg', + large: 'public/plugins/grafana-pyroscope-datasource/img/grafana_pyroscope_icon.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://www.grafana.com', + }, + description: + 'Data source for Grafana Pyroscope, horizontally-scalable, highly-available, multi-tenant continuous profiling aggregation system.', + links: [ + { + name: 'GitHub Project', + url: 'https://github.com/grafana/pyroscope', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/pyroscope/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/pyroscope/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0-0', + grafanaVersion: '*', + }, + backend: true, + category: 'profiling', + executable: 'gpx_grafana-pyroscope-datasource', + metrics: true, + }, + class: 'core', + module: { + path: 'public/plugins/grafana-pyroscope-datasource/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana-pyroscope-datasource', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'grafana-testdata-datasource', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'grafana-testdata-datasource', + type: 'datasource', + name: 'TestData', + info: { + keywords: [], + logos: { + small: 'public/plugins/grafana-testdata-datasource/img/testdata.svg', + large: 'public/plugins/grafana-testdata-datasource/img/testdata.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Generates test data in different forms', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/testdata/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0-0', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + executable: 'gpx_testdata', + includes: [ + { + type: 'dashboard', + name: 'Streaming Example', + role: 'Viewer', + path: 'dashboards/streaming.json', + }, + ], + logs: true, + metrics: true, + queryOptions: { + maxDataPoints: true, + minInterval: true, + }, + }, + class: 'core', + module: { + path: 'public/plugins/grafana-testdata-datasource/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/grafana-testdata-datasource', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'graphite', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'graphite', + type: 'datasource', + name: 'Graphite', + info: { + keywords: [], + logos: { + small: 'public/plugins/graphite/img/graphite_logo.png', + large: 'public/plugins/graphite/img/graphite_logo.png', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Open source time series database', + links: [ + { + name: 'Learn more', + url: 'https://graphiteapp.org/', + }, + { + name: 'Graphite 1.1 Release', + url: 'https://grafana.com/blog/2018/01/11/graphite-1.1-teaching-an-old-dog-new-tricks/', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/graphite/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'tsdb', + executable: 'gpx_graphite', + includes: [ + { + type: 'dashboard', + name: 'Graphite Carbon Metrics', + role: 'Viewer', + path: 'dashboards/carbon_metrics.json', + }, + { + type: 'dashboard', + name: 'Metrictank (Graphite alternative)', + role: 'Viewer', + path: 'dashboards/metrictank.json', + }, + ], + metrics: true, + queryOptions: { + maxDataPoints: true, + cacheTimeout: true, + }, + }, + class: 'core', + module: { + path: 'public/plugins/graphite/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/graphite', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'heatmap', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'heatmap', + type: 'panel', + name: 'Heatmap', + info: { + keywords: [], + logos: { + small: 'public/plugins/heatmap/img/icn-heatmap-panel.svg', + large: 'public/plugins/heatmap/img/icn-heatmap-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Like a histogram over time', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/heatmap/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/heatmap', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/heatmap', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'histogram', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'histogram', + type: 'panel', + name: 'Histogram', + info: { + keywords: ['distribution', 'bar chart', 'frequency', 'proportional'], + logos: { + small: 'public/plugins/histogram/img/histogram.svg', + large: 'public/plugins/histogram/img/histogram.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Distribution of values presented as a bar chart.', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/histogram/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/histogram', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/histogram', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'influxdb', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'influxdb', + type: 'datasource', + name: 'InfluxDB', + info: { + keywords: [], + logos: { + small: 'public/plugins/influxdb/img/influxdb_logo.svg', + large: 'public/plugins/influxdb/img/influxdb_logo.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Open source time series database', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/influxdb/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'tsdb', + logs: true, + metrics: true, + queryOptions: { + minInterval: true, + }, + }, + class: 'core', + module: { + path: 'core:plugin/influxdb', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/influxdb', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'jaeger', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'jaeger', + type: 'datasource', + name: 'Jaeger', + info: { + keywords: [], + logos: { + small: 'public/plugins/jaeger/img/jaeger_logo.svg', + large: 'public/plugins/jaeger/img/jaeger_logo.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Open source, end-to-end distributed tracing', + links: [ + { + name: 'Learn more', + url: 'https://www.jaegertracing.io', + }, + { + name: 'Jaeger GitHub Project', + url: 'https://github.com/jaegertracing/jaeger', + }, + { + name: 'Repository', + url: 'https://github.com/grafana/grafana', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/jaeger/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0-0', + grafanaVersion: '*', + }, + backend: true, + category: 'tracing', + executable: 'gpx_jaeger', + metrics: true, + tracing: true, + }, + class: 'core', + module: { + path: 'public/plugins/jaeger/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/jaeger', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'live', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'live', + type: 'panel', + name: 'Live', + info: { + keywords: [], + logos: { + small: 'public/plugins/live/img/live.svg', + large: 'public/plugins/live/img/live.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + skipDataQuery: true, + state: 'alpha', + }, + class: 'core', + module: { + path: 'core:plugin/live', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/live', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'logs', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'logs', + type: 'panel', + name: 'Logs', + info: { + keywords: [], + logos: { + small: 'public/plugins/logs/img/icn-logs-panel.svg', + large: 'public/plugins/logs/img/icn-logs-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/logs/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/logs', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/logs', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'loki', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'loki', + type: 'datasource', + name: 'Loki', + info: { + keywords: [], + logos: { + small: 'public/plugins/loki/img/loki_icon.svg', + large: 'public/plugins/loki/img/loki_icon.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Like Prometheus but for logs. OSS logging solution from Grafana Labs', + links: [ + { + name: 'Learn more', + url: 'https://grafana.com/loki', + }, + { + name: 'GitHub Project', + url: 'https://github.com/grafana/loki', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/loki/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.4.0', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'logging', + executable: 'gpx_loki', + logs: true, + metrics: true, + queryOptions: { + maxDataPoints: true, + }, + streaming: true, + }, + class: 'core', + module: { + path: 'public/plugins/loki/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/loki', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'mixed', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'mixed', + type: 'datasource', + name: '-- Mixed --', + info: { + keywords: [], + logos: { + small: 'public/plugins/mixed/img/icn-mixeddatasources.svg', + large: 'public/plugins/mixed/img/icn-mixeddatasources.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Lets you query multiple data sources in the same panel.', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/#special-data-sources', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + builtIn: true, + metrics: true, + queryOptions: { + minInterval: true, + }, + }, + class: 'core', + module: { + path: 'core:plugin/mixed', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/mixed', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'mssql', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'mssql', + type: 'datasource', + name: 'Microsoft SQL Server', + info: { + keywords: [], + logos: { + small: 'public/plugins/mssql/img/sql_server_logo.svg', + large: 'public/plugins/mssql/img/sql_server_logo.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Data source for Microsoft SQL Server compatible databases', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/mssql/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.4.0', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'sql', + executable: 'gpx_mssql', + metrics: true, + queryOptions: { + minInterval: true, + }, + }, + class: 'core', + module: { + path: 'public/plugins/mssql/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/mssql', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + translations: { + 'cs-CZ': 'public/plugins/mssql/locales/cs-CZ/mssql.json', + 'de-DE': 'public/plugins/mssql/locales/de-DE/mssql.json', + 'en-US': 'public/plugins/mssql/locales/en-US/mssql.json', + 'es-ES': 'public/plugins/mssql/locales/es-ES/mssql.json', + 'fr-FR': 'public/plugins/mssql/locales/fr-FR/mssql.json', + 'hu-HU': 'public/plugins/mssql/locales/hu-HU/mssql.json', + 'id-ID': 'public/plugins/mssql/locales/id-ID/mssql.json', + 'it-IT': 'public/plugins/mssql/locales/it-IT/mssql.json', + 'ja-JP': 'public/plugins/mssql/locales/ja-JP/mssql.json', + 'ko-KR': 'public/plugins/mssql/locales/ko-KR/mssql.json', + 'nl-NL': 'public/plugins/mssql/locales/nl-NL/mssql.json', + 'pl-PL': 'public/plugins/mssql/locales/pl-PL/mssql.json', + 'pt-BR': 'public/plugins/mssql/locales/pt-BR/mssql.json', + 'pt-PT': 'public/plugins/mssql/locales/pt-PT/mssql.json', + 'ru-RU': 'public/plugins/mssql/locales/ru-RU/mssql.json', + 'sv-SE': 'public/plugins/mssql/locales/sv-SE/mssql.json', + 'tr-TR': 'public/plugins/mssql/locales/tr-TR/mssql.json', + 'zh-Hans': 'public/plugins/mssql/locales/zh-Hans/mssql.json', + 'zh-Hant': 'public/plugins/mssql/locales/zh-Hant/mssql.json', + }, + }, + status: {}, + }, + v0alpha1Meta, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'mysql', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'mysql', + type: 'datasource', + name: 'MySQL', + info: { + keywords: [], + logos: { + small: 'public/plugins/mysql/img/mysql_logo.svg', + large: 'public/plugins/mysql/img/mysql_logo.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Data source for MySQL databases', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/mysql/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.4.0', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'sql', + executable: 'gpx_mysql', + metrics: true, + queryOptions: { + minInterval: true, + }, + }, + class: 'core', + module: { + path: 'public/plugins/mysql/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/mysql', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'news', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'news', + type: 'panel', + name: 'News', + info: { + keywords: [], + logos: { + small: 'public/plugins/news/img/news.svg', + large: 'public/plugins/news/img/news.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'RSS feed reader', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/news/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + skipDataQuery: true, + state: 'beta', + }, + class: 'core', + module: { + path: 'core:plugin/news', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/news', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'nodeGraph', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'nodeGraph', + type: 'panel', + name: 'Node Graph', + info: { + keywords: [], + logos: { + small: 'public/plugins/nodeGraph/img/icn-node-graph.svg', + large: 'public/plugins/nodeGraph/img/icn-node-graph.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/node-graph/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/nodeGraph', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/nodeGraph', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'opentsdb', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'opentsdb', + type: 'datasource', + name: 'OpenTSDB', + info: { + keywords: [], + logos: { + small: 'public/plugins/opentsdb/img/opentsdb_logo.png', + large: 'public/plugins/opentsdb/img/opentsdb_logo.png', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Open source time series database', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/opentsdb/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0-0', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'tsdb', + executable: 'gpx_opentsdb', + metrics: true, + }, + class: 'core', + module: { + path: 'public/plugins/opentsdb/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/opentsdb', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'parca', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'parca', + type: 'datasource', + name: 'Parca', + info: { + keywords: ['grafana', 'datasource', 'parca', 'profiling'], + logos: { + small: 'public/plugins/parca/img/logo-small.svg', + large: 'public/plugins/parca/img/logo-small.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://www.grafana.com', + }, + description: + 'Continuous profiling for analysis of CPU and memory usage, down to the line number and throughout time. Saving infrastructure cost, improving performance, and increasing reliability.', + links: [ + { + name: 'GitHub Project', + url: 'https://github.com/parca-dev/parca', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/parca/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0-0', + grafanaVersion: '*', + }, + backend: true, + category: 'profiling', + executable: 'gpx_parca', + metrics: true, + }, + class: 'core', + module: { + path: 'public/plugins/parca/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/parca', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'piechart', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'piechart', + type: 'panel', + name: 'Pie chart', + info: { + keywords: [], + logos: { + small: 'public/plugins/piechart/img/icon_piechart.svg', + large: 'public/plugins/piechart/img/icon_piechart.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'The new core pie chart visualization', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/pie-chart/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/piechart', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/piechart', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'prometheus', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'prometheus', + type: 'datasource', + name: 'Prometheus', + info: { + keywords: [], + logos: { + small: 'public/plugins/prometheus/img/prometheus_logo.svg', + large: 'public/plugins/prometheus/img/prometheus_logo.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Open source time series database & alerting', + links: [ + { + name: 'Learn more', + url: 'https://prometheus.io/', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/prometheus/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'tsdb', + includes: [ + { + type: 'dashboard', + name: 'Prometheus Stats', + role: 'Viewer', + path: 'dashboards/prometheus_stats.json', + }, + { + type: 'dashboard', + name: 'Prometheus 2.0 Stats', + role: 'Viewer', + path: 'dashboards/prometheus_2_stats.json', + }, + { + type: 'dashboard', + name: 'Grafana Stats', + role: 'Viewer', + path: 'dashboards/grafana_stats.json', + }, + ], + metrics: true, + multiValueFilterOperators: true, + queryOptions: { + minInterval: true, + }, + routes: [ + { + path: 'api/v1/query', + method: 'POST', + reqRole: 'Viewer', + reqAction: 'datasources:query', + }, + { + path: 'api/v1/query_range', + method: 'POST', + reqRole: 'Viewer', + reqAction: 'datasources:query', + }, + { + path: 'api/v1/series', + method: 'POST', + reqRole: 'Viewer', + reqAction: 'datasources:query', + }, + { + path: 'api/v1/labels', + method: 'POST', + reqRole: 'Viewer', + reqAction: 'datasources:query', + }, + { + path: 'api/v1/query_exemplars', + method: 'POST', + reqRole: 'Viewer', + reqAction: 'datasources:query', + }, + { + path: '/rules', + method: 'GET', + reqRole: 'Viewer', + reqAction: 'alert.rules.external:read', + }, + { + path: '/rules', + method: 'POST', + reqRole: 'Editor', + reqAction: 'alert.rules.external:write', + }, + { + path: '/rules', + method: 'DELETE', + reqRole: 'Editor', + reqAction: 'alert.rules.external:write', + }, + { + path: '/config/v1/rules', + method: 'DELETE', + reqRole: 'Editor', + reqAction: 'alert.rules.external:write', + }, + { + path: '/config/v1/rules', + method: 'POST', + reqRole: 'Editor', + reqAction: 'alert.rules.external:write', + }, + ], + }, + class: 'core', + module: { + path: 'core:plugin/prometheus', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/prometheus', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'radialbar', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'radialbar', + type: 'panel', + name: 'New Gauge', + info: { + keywords: [], + logos: { + small: 'public/plugins/radialbar/img/icon_gauge.svg', + large: 'public/plugins/radialbar/img/icon_gauge.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Standard gauge visualization', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/gauge/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + state: 'alpha', + }, + class: 'core', + module: { + path: 'core:plugin/radialbar', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/radialbar', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'stackdriver', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'stackdriver', + type: 'datasource', + name: 'Google Cloud Monitoring', + info: { + keywords: [], + logos: { + small: 'public/plugins/stackdriver/img/cloud_monitoring_logo.svg', + large: 'public/plugins/stackdriver/img/cloud_monitoring_logo.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: "Data source for Google's monitoring service (formerly named Stackdriver)", + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/google-cloud-monitoring/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + alerting: true, + annotations: true, + backend: true, + category: 'cloud', + executable: 'gpx_cloudmonitoring', + includes: [ + { + type: 'dashboard', + name: 'Data Processing Monitoring', + role: 'Viewer', + path: 'dashboards/dataprocessing-monitoring.json', + }, + { + type: 'dashboard', + name: 'Cloud Functions Monitoring', + role: 'Viewer', + path: 'dashboards/cloudfunctions-monitoring.json', + }, + { + type: 'dashboard', + name: 'GCE VM Instance Monitoring', + role: 'Viewer', + path: 'dashboards/gce-vm-instance-monitoring.json', + }, + { + type: 'dashboard', + name: 'GKE Prometheus Pod/Node Monitoring', + role: 'Viewer', + path: 'dashboards/gke-prometheus-pod-node-monitoring.json', + }, + { + type: 'dashboard', + name: 'Firewall Insights Monitoring', + role: 'Viewer', + path: 'dashboards/firewall-insight-monitoring.json', + }, + { + type: 'dashboard', + name: 'GCE Network Monitoring', + role: 'Viewer', + path: 'dashboards/gce-network-monitoring.json', + }, + { + type: 'dashboard', + name: 'HTTP/S LB Backend Services', + role: 'Viewer', + path: 'dashboards/https-lb-backend-services-monitoring.json', + }, + { + type: 'dashboard', + name: 'HTTP/S Load Balancer Monitoring', + role: 'Viewer', + path: 'dashboards/https-loadbalancer-monitoring.json', + }, + { + type: 'dashboard', + name: 'Network TCP Load Balancer Monitoring', + role: 'Viewer', + path: 'dashboards/network-tcp-loadbalancer-monitoring.json', + }, + { + type: 'dashboard', + name: 'MicroService Monitoring', + role: 'Viewer', + path: 'dashboards/micro-service-monitoring.json', + }, + { + type: 'dashboard', + name: 'Cloud Storage Monitoring', + role: 'Viewer', + path: 'dashboards/cloud-storage-monitoring.json', + }, + { + type: 'dashboard', + name: 'Cloud SQL Monitoring', + role: 'Viewer', + path: 'dashboards/cloudsql-monitoring.json', + }, + { + type: 'dashboard', + name: 'Cloud SQL(MySQL) Monitoring', + role: 'Viewer', + path: 'dashboards/cloudsql-mysql-monitoring.json', + }, + ], + logs: true, + metrics: true, + queryOptions: { + maxDataPoints: true, + cacheTimeout: true, + }, + }, + class: 'core', + module: { + path: 'public/plugins/stackdriver/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/stackdriver', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'stat', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'stat', + type: 'panel', + name: 'Stat', + info: { + keywords: [], + logos: { + small: 'public/plugins/stat/img/icn-singlestat-panel.svg', + large: 'public/plugins/stat/img/icn-singlestat-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Big stat values & sparklines', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/stat/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/stat', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/stat', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'state-timeline', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'state-timeline', + type: 'panel', + name: 'State timeline', + info: { + keywords: [], + logos: { + small: 'public/plugins/state-timeline/img/timeline.svg', + large: 'public/plugins/state-timeline/img/timeline.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'State changes and durations', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/state-timeline/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/state-timeline', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/state-timeline', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'status-history', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'status-history', + type: 'panel', + name: 'Status history', + info: { + keywords: [], + logos: { + small: 'public/plugins/status-history/img/status.svg', + large: 'public/plugins/status-history/img/status.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Periodic status history', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/status-history/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/status-history', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/status-history', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'table', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'table', + type: 'panel', + name: 'Table', + info: { + keywords: [], + logos: { + small: 'public/plugins/table/img/icn-table-panel.svg', + large: 'public/plugins/table/img/icn-table-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Supports many column styles', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/table/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/table', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/table', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'tempo', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'tempo', + type: 'datasource', + name: 'Tempo', + info: { + keywords: [], + logos: { + small: 'public/plugins/tempo/img/tempo_logo.svg', + large: 'public/plugins/tempo/img/tempo_logo.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'High volume, minimal dependency trace storage. OSS tracing solution from Grafana Labs.', + links: [ + { + name: 'GitHub Project', + url: 'https://github.com/grafana/tempo', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/tempo/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0-0', + grafanaVersion: '*', + }, + backend: true, + category: 'tracing', + executable: 'gpx_tempo', + metrics: true, + tracing: true, + }, + class: 'core', + module: { + path: 'public/plugins/tempo/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/tempo', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'text', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'text', + type: 'panel', + name: 'Text', + info: { + keywords: [], + logos: { + small: 'public/plugins/text/img/icn-text-panel.svg', + large: 'public/plugins/text/img/icn-text-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Supports markdown and html content', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/text/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + skipDataQuery: true, + }, + class: 'core', + module: { + path: 'core:plugin/text', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/text', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'timeseries', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'timeseries', + type: 'panel', + name: 'Time series', + info: { + keywords: [], + logos: { + small: 'public/plugins/timeseries/img/icn-timeseries-panel.svg', + large: 'public/plugins/timeseries/img/icn-timeseries-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Time based line, area and bar charts', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/time-series/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/timeseries', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/timeseries', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'traces', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'traces', + type: 'panel', + name: 'Traces', + info: { + keywords: [], + logos: { + small: 'public/plugins/traces/img/traces-panel.svg', + large: 'public/plugins/traces/img/traces-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/traces/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/traces', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/traces', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'trend', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'trend', + type: 'panel', + name: 'Trend', + info: { + keywords: [], + logos: { + small: 'public/plugins/trend/img/trend.svg', + large: 'public/plugins/trend/img/trend.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Like timeseries, but when x != time', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/trend/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + state: 'beta', + }, + class: 'core', + module: { + path: 'core:plugin/trend', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/trend', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'welcome', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'welcome', + type: 'panel', + name: 'Welcome', + info: { + keywords: [], + logos: { + small: 'public/plugins/welcome/img/icn-dashlist-panel.svg', + large: 'public/plugins/welcome/img/icn-dashlist-panel.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + hideFromList: true, + skipDataQuery: true, + }, + class: 'core', + module: { + path: 'core:plugin/welcome', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/welcome', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'xychart', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'xychart', + type: 'panel', + name: 'XY Chart', + info: { + keywords: ['scatter', 'plot'], + logos: { + small: 'public/plugins/xychart/img/icn-xychart.svg', + large: 'public/plugins/xychart/img/icn-xychart.svg', + }, + updated: '', + version: '', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Supports arbitrary X vs Y in a graph to visualize the relationship between two variables.', + links: [ + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/xy-chart/', + }, + ], + }, + dependencies: { + grafanaDependency: '', + grafanaVersion: '*', + }, + }, + class: 'core', + module: { + path: 'core:plugin/xychart', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/xychart', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + { + kind: 'Meta', + apiVersion: 'plugins.grafana.app/v0alpha1', + metadata: { + name: 'zipkin', + namespace: 'default', + }, + spec: { + pluginJson: { + id: 'zipkin', + type: 'datasource', + name: 'Zipkin', + info: { + keywords: [], + logos: { + small: 'public/plugins/zipkin/img/zipkin-logo.svg', + large: 'public/plugins/zipkin/img/zipkin-logo.svg', + }, + updated: '', + version: '12.4.0-pre', + author: { + name: 'Grafana Labs', + url: 'https://grafana.com', + }, + description: 'Placeholder for the distributed tracing system.', + links: [ + { + name: 'Learn more', + url: 'https://zipkin.io', + }, + { + name: 'Raise issue', + url: 'https://github.com/grafana/grafana/issues/new', + }, + { + name: 'Documentation', + url: 'https://grafana.com/docs/grafana/latest/datasources/zipkin/', + }, + ], + }, + dependencies: { + grafanaDependency: '>=10.3.0-0', + grafanaVersion: '*', + }, + backend: true, + category: 'tracing', + executable: 'gpx_zipkin', + metrics: true, + tracing: true, + }, + class: 'core', + module: { + path: 'public/plugins/zipkin/module.js', + loadingStrategy: 'script', + }, + baseURL: 'public/plugins/zipkin', + signature: { + status: 'internal', + }, + angular: { + detected: false, + }, + }, + status: {}, + }, + ], +}); diff --git a/packages/grafana-runtime/src/services/pluginMeta/types.ts b/packages/grafana-runtime/src/services/pluginMeta/types.ts new file mode 100644 index 00000000000..81efe0df7b3 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/types.ts @@ -0,0 +1,10 @@ +import type { AppPluginConfig } from '@grafana/data'; + +import type { Meta } from './types/meta_object_gen'; + +export type AppPluginMetas = Record; + +export type AppPluginMetasMapper = (response: T) => AppPluginMetas; +export interface PluginMetasResponse { + items: Meta[]; +} diff --git a/packages/grafana-runtime/src/services/pluginMeta/types/meta_object_gen.ts b/packages/grafana-runtime/src/services/pluginMeta/types/meta_object_gen.ts new file mode 100644 index 00000000000..044ec1f4cd8 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/types/meta_object_gen.ts @@ -0,0 +1,49 @@ +/* + * This file was generated by grafana-app-sdk. DO NOT EDIT. + */ +import { Spec } from './types.spec.gen'; +import { Status } from './types.status.gen'; + +export interface Metadata { + name: string; + namespace: string; + generateName?: string; + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + deletionTimestamp?: string; + deletionGracePeriodSeconds?: number; + labels?: Record; + annotations?: Record; + ownerReferences?: OwnerReference[]; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface OwnerReference { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +} + +export interface ManagedFieldsEntry { + manager?: string; + operation?: string; + apiVersion?: string; + time?: string; + fieldsType?: string; + subresource?: string; +} + +export interface Meta { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; + status: Status; +} diff --git a/packages/grafana-runtime/src/services/pluginMeta/types/types.spec.gen.ts b/packages/grafana-runtime/src/services/pluginMeta/types/types.spec.gen.ts new file mode 100644 index 00000000000..51845e98454 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/types/types.spec.gen.ts @@ -0,0 +1,278 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// JSON configuration schema for Grafana plugins +// Converted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json +export interface JSONData { + // Unique name of the plugin + id: string; + // Plugin type + type: "app" | "datasource" | "panel" | "renderer"; + // Human-readable name of the plugin + name: string; + // Metadata for the plugin + info: Info; + // Dependency information + dependencies: Dependencies; + // Optional fields + alerting?: boolean; + annotations?: boolean; + autoEnabled?: boolean; + backend?: boolean; + buildMode?: string; + builtIn?: boolean; + category?: "tsdb" | "logging" | "cloud" | "tracing" | "profiling" | "sql" | "enterprise" | "iot" | "other"; + enterpriseFeatures?: EnterpriseFeatures; + executable?: string; + hideFromList?: boolean; + // +listType=atomic + includes?: Include[]; + logs?: boolean; + metrics?: boolean; + multiValueFilterOperators?: boolean; + pascalName?: string; + preload?: boolean; + queryOptions?: QueryOptions; + // +listType=atomic + routes?: Route[]; + skipDataQuery?: boolean; + state?: "alpha" | "beta"; + streaming?: boolean; + suggestions?: boolean; + tracing?: boolean; + iam?: IAM; + // +listType=atomic + roles?: Role[]; + extensions?: Extensions; +} + +export const defaultJSONData = (): JSONData => ({ + id: "", + type: "app", + name: "", + info: defaultInfo(), + dependencies: defaultDependencies(), +}); + +export interface Info { + // Required fields + // +listType=set + keywords: string[]; + logos: { + small: string; + large: string; + }; + updated: string; + version: string; + // Optional fields + author?: { + name?: string; + email?: string; + url?: string; + }; + description?: string; + // +listType=atomic + links?: { + name?: string; + url?: string; + }[]; + // +listType=atomic + screenshots?: { + name?: string; + path?: string; + }[]; +} + +export const defaultInfo = (): Info => ({ + keywords: [], + logos: { + small: "", + large: "", +}, + updated: "", + version: "", +}); + +export interface Dependencies { + // Required field + grafanaDependency: string; + // Optional fields + grafanaVersion?: string; + // +listType=set + // +listMapKey=id + plugins?: { + id: string; + type: "app" | "datasource" | "panel"; + name: string; + }[]; + extensions?: { + // +listType=set + exposedComponents?: string[]; + }; +} + +export const defaultDependencies = (): Dependencies => ({ + grafanaDependency: "", +}); + +export interface EnterpriseFeatures { + // Allow additional properties + healthDiagnosticsErrors?: boolean; +} + +export const defaultEnterpriseFeatures = (): EnterpriseFeatures => ({ + healthDiagnosticsErrors: false, +}); + +export interface Include { + uid?: string; + type?: "dashboard" | "page" | "panel" | "datasource"; + name?: string; + component?: string; + role?: "Admin" | "Editor" | "Viewer" | "None"; + action?: string; + path?: string; + addToNav?: boolean; + defaultNav?: boolean; + icon?: string; +} + +export const defaultInclude = (): Include => ({ +}); + +export interface QueryOptions { + maxDataPoints?: boolean; + minInterval?: boolean; + cacheTimeout?: boolean; +} + +export const defaultQueryOptions = (): QueryOptions => ({ +}); + +export interface Route { + path?: string; + method?: string; + url?: string; + reqSignedIn?: boolean; + reqRole?: string; + reqAction?: string; + // +listType=atomic + headers?: string[]; + body?: Record; + tokenAuth?: { + url?: string; + // +listType=set + scopes?: string[]; + params?: Record; + }; + jwtTokenAuth?: { + url?: string; + // +listType=set + scopes?: string[]; + params?: Record; + }; + // +listType=atomic + urlParams?: { + name?: string; + content?: string; + }[]; +} + +export const defaultRoute = (): Route => ({ +}); + +export interface IAM { + // +listType=atomic + permissions?: { + action?: string; + scope?: string; + }[]; +} + +export const defaultIAM = (): IAM => ({ +}); + +export interface Role { + role?: { + name?: string; + description?: string; + // +listType=atomic + permissions?: { + action?: string; + scope?: string; + }[]; + }; + // +listType=set + grants?: string[]; +} + +export const defaultRole = (): Role => ({ +}); + +export interface Extensions { + // +listType=atomic + addedComponents?: { + // +listType=set + targets: string[]; + title: string; + description?: string; + }[]; + // +listType=atomic + addedLinks?: { + // +listType=set + targets: string[]; + title: string; + description?: string; + }[]; + // +listType=atomic + addedFunctions?: { + // +listType=set + targets: string[]; + title: string; + description?: string; + }[]; + // +listType=set + // +listMapKey=id + exposedComponents?: { + id: string; + title?: string; + description?: string; + }[]; + // +listType=set + // +listMapKey=id + extensionPoints?: { + id: string; + title?: string; + description?: string; + }[]; +} + +export const defaultExtensions = (): Extensions => ({ +}); + +export interface Spec { + pluginJson: JSONData; + class: "core" | "external"; + module?: { + path: string; + hash?: string; + loadingStrategy?: "fetch" | "script"; + }; + baseURL?: string; + signature?: { + status: "internal" | "valid" | "invalid" | "modified" | "unsigned"; + type?: "grafana" | "commercial" | "community" | "private" | "private-glob"; + org?: string; + }; + angular?: { + detected: boolean; + }; + translations?: Record; + // +listType=atomic + children?: string[]; +} + +export const defaultSpec = (): Spec => ({ + pluginJson: defaultJSONData(), + class: "core", +}); + diff --git a/packages/grafana-runtime/src/services/pluginMeta/types/types.status.gen.ts b/packages/grafana-runtime/src/services/pluginMeta/types/types.status.gen.ts new file mode 100644 index 00000000000..01be8df7961 --- /dev/null +++ b/packages/grafana-runtime/src/services/pluginMeta/types/types.status.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface OperatorState { + // lastEvaluation is the ResourceVersion last evaluated + lastEvaluation: string; + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + state: "success" | "in_progress" | "failed"; + // descriptiveState is an optional more descriptive state field which has no requirements on format + descriptiveState?: string; + // details contains any extra information that is operator-specific + details?: Record; +} + +export const defaultOperatorState = (): OperatorState => ({ + lastEvaluation: "", + state: "success", +}); + +export interface Status { + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + operatorStates?: Record; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ +}); +