SchemaV2: Add library panel repeat options to v2 schema during conversion (#114109)
* Add library panel repeat options to v2 schema during conversion
* use any instead of interface{}
* change to common.Unstructured instead of byte[] for model field
* Fix the tests and let the library panel behavior fetch repeat options in public and scripted dashboards
* fix library panel differences between backend and frontend conversion
This commit is contained in:
@@ -11,11 +11,13 @@ import (
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
)
|
||||
|
||||
func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSourceIndexProvider, _ schemaversion.LibraryElementIndexProvider) error {
|
||||
func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error {
|
||||
// Wrap the provider once with 10s caching for all conversions.
|
||||
// This prevents repeated DB queries across multiple conversion calls while allowing
|
||||
// the cache to refresh periodically, making it suitable for long-lived singleton usage.
|
||||
dsIndexProvider = schemaversion.WrapIndexProviderWithCache(dsIndexProvider)
|
||||
// Wrap library element provider with caching as well
|
||||
leIndexProvider = schemaversion.WrapLibraryElementProviderWithCache(leIndexProvider)
|
||||
|
||||
// v0 conversions
|
||||
if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv1.Dashboard)(nil),
|
||||
@@ -26,13 +28,13 @@ func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSo
|
||||
}
|
||||
if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv2alpha1.Dashboard)(nil),
|
||||
withConversionMetrics(dashv0.APIVERSION, dashv2alpha1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_V0_to_V2alpha1(a.(*dashv0.Dashboard), b.(*dashv2alpha1.Dashboard), scope, dsIndexProvider)
|
||||
return Convert_V0_to_V2alpha1(a.(*dashv0.Dashboard), b.(*dashv2alpha1.Dashboard), scope, dsIndexProvider, leIndexProvider)
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv2beta1.Dashboard)(nil),
|
||||
withConversionMetrics(dashv0.APIVERSION, dashv2beta1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_V0_to_V2beta1(a.(*dashv0.Dashboard), b.(*dashv2beta1.Dashboard), scope, dsIndexProvider)
|
||||
return Convert_V0_to_V2beta1(a.(*dashv0.Dashboard), b.(*dashv2beta1.Dashboard), scope, dsIndexProvider, leIndexProvider)
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -46,13 +48,13 @@ func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSo
|
||||
}
|
||||
if err := s.AddConversionFunc((*dashv1.Dashboard)(nil), (*dashv2alpha1.Dashboard)(nil),
|
||||
withConversionMetrics(dashv1.APIVERSION, dashv2alpha1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_V1beta1_to_V2alpha1(a.(*dashv1.Dashboard), b.(*dashv2alpha1.Dashboard), scope, dsIndexProvider)
|
||||
return Convert_V1beta1_to_V2alpha1(a.(*dashv1.Dashboard), b.(*dashv2alpha1.Dashboard), scope, dsIndexProvider, leIndexProvider)
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddConversionFunc((*dashv1.Dashboard)(nil), (*dashv2beta1.Dashboard)(nil),
|
||||
withConversionMetrics(dashv1.APIVERSION, dashv2beta1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_V1beta1_to_V2beta1(a.(*dashv1.Dashboard), b.(*dashv2beta1.Dashboard), scope, dsIndexProvider)
|
||||
return Convert_V1beta1_to_V2beta1(a.(*dashv1.Dashboard), b.(*dashv2beta1.Dashboard), scope, dsIndexProvider, leIndexProvider)
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ import (
|
||||
func TestConversionMatrixExist(t *testing.T) {
|
||||
// Initialize the migrator with a test data source provider
|
||||
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
|
||||
leProvider := migrationtestutil.NewLibraryElementProvider()
|
||||
// Use TestLibraryElementProvider for tests that need library panel models with repeat options
|
||||
leProvider := migrationtestutil.NewTestLibraryElementProvider()
|
||||
migration.Initialize(dsProvider, leProvider)
|
||||
|
||||
versions := []metav1.Object{
|
||||
@@ -86,7 +87,8 @@ func TestDeepCopyValid(t *testing.T) {
|
||||
func TestDashboardConversionToAllVersions(t *testing.T) {
|
||||
// Initialize the migrator with a test data source provider
|
||||
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
|
||||
leProvider := migrationtestutil.NewLibraryElementProvider()
|
||||
// Use TestLibraryElementProvider for tests that need library panel models with repeat options
|
||||
leProvider := migrationtestutil.NewTestLibraryElementProvider()
|
||||
migration.Initialize(dsProvider, leProvider)
|
||||
|
||||
// Set up conversion scheme
|
||||
@@ -246,7 +248,8 @@ func TestDashboardConversionToAllVersions(t *testing.T) {
|
||||
func TestMigratedDashboardsConversion(t *testing.T) {
|
||||
// Initialize the migrator with a test data source provider
|
||||
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
|
||||
leProvider := migrationtestutil.NewLibraryElementProvider()
|
||||
// Use TestLibraryElementProvider for tests that need library panel models with repeat options
|
||||
leProvider := migrationtestutil.NewTestLibraryElementProvider()
|
||||
migration.Initialize(dsProvider, leProvider)
|
||||
|
||||
// Set up conversion scheme
|
||||
@@ -381,7 +384,8 @@ func testConversion(t *testing.T, convertedDash metav1.Object, filename, outputD
|
||||
func TestConversionMetrics(t *testing.T) {
|
||||
// Initialize migration with test providers
|
||||
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
|
||||
leProvider := migrationtestutil.NewLibraryElementProvider()
|
||||
// Use TestLibraryElementProvider for tests that need library panel models with repeat options
|
||||
leProvider := migrationtestutil.NewTestLibraryElementProvider()
|
||||
migration.Initialize(dsProvider, leProvider)
|
||||
|
||||
// Create a test registry for metrics
|
||||
@@ -509,7 +513,8 @@ func TestConversionMetrics(t *testing.T) {
|
||||
// TestConversionMetricsWrapper tests the withConversionMetrics wrapper function
|
||||
func TestConversionMetricsWrapper(t *testing.T) {
|
||||
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
|
||||
leProvider := migrationtestutil.NewLibraryElementProvider()
|
||||
// Use TestLibraryElementProvider for tests that need library panel models with repeat options
|
||||
leProvider := migrationtestutil.NewTestLibraryElementProvider()
|
||||
migration.Initialize(dsProvider, leProvider)
|
||||
|
||||
// Create a test registry for metrics
|
||||
@@ -678,7 +683,8 @@ func TestSchemaVersionExtraction(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Test the schema version extraction logic by creating a wrapper and checking the metrics labels
|
||||
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
|
||||
leProvider := migrationtestutil.NewLibraryElementProvider()
|
||||
// Use TestLibraryElementProvider for tests that need library panel models with repeat options
|
||||
leProvider := migrationtestutil.NewTestLibraryElementProvider()
|
||||
migration.Initialize(dsProvider, leProvider)
|
||||
|
||||
// Create a test registry for metrics
|
||||
@@ -723,7 +729,8 @@ func TestSchemaVersionExtraction(t *testing.T) {
|
||||
// TestConversionLogging tests that conversion-level logging works correctly
|
||||
func TestConversionLogging(t *testing.T) {
|
||||
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
|
||||
leProvider := migrationtestutil.NewLibraryElementProvider()
|
||||
// Use TestLibraryElementProvider for tests that need library panel models with repeat options
|
||||
leProvider := migrationtestutil.NewTestLibraryElementProvider()
|
||||
migration.Initialize(dsProvider, leProvider)
|
||||
|
||||
// Create a test registry for metrics
|
||||
@@ -815,7 +822,8 @@ func TestConversionLogging(t *testing.T) {
|
||||
// TestConversionLogLevels tests that appropriate log levels are used
|
||||
func TestConversionLogLevels(t *testing.T) {
|
||||
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
|
||||
leProvider := migrationtestutil.NewLibraryElementProvider()
|
||||
// Use TestLibraryElementProvider for tests that need library panel models with repeat options
|
||||
leProvider := migrationtestutil.NewTestLibraryElementProvider()
|
||||
migration.Initialize(dsProvider, leProvider)
|
||||
|
||||
t.Run("log levels and structured fields verification", func(t *testing.T) {
|
||||
@@ -887,7 +895,8 @@ func TestConversionLogLevels(t *testing.T) {
|
||||
// TestConversionLoggingFields tests that all expected fields are included in log messages
|
||||
func TestConversionLoggingFields(t *testing.T) {
|
||||
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
|
||||
leProvider := migrationtestutil.NewLibraryElementProvider()
|
||||
// Use TestLibraryElementProvider for tests that need library panel models with repeat options
|
||||
leProvider := migrationtestutil.NewTestLibraryElementProvider()
|
||||
migration.Initialize(dsProvider, leProvider)
|
||||
|
||||
t.Run("verify all log fields are present", func(t *testing.T) {
|
||||
|
||||
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"kind": "Dashboard",
|
||||
"apiVersion": "dashboard.grafana.app/v1beta1",
|
||||
"metadata": {
|
||||
"name": "library-panel-repeat-options-test",
|
||||
"labels": {
|
||||
"test": "library-panel-repeat"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "Library Panel Repeat Options Test Dashboard",
|
||||
"description": "Testing library panel repeat options migration from v1beta1 to v2alpha1",
|
||||
"tags": ["test", "library-panels", "repeat"],
|
||||
"schemaVersion": 38,
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Library Panel with Horizontal Repeat",
|
||||
"type": "library-panel-ref",
|
||||
"gridPos": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 12,
|
||||
"h": 8
|
||||
},
|
||||
"libraryPanel": {
|
||||
"uid": "lib-panel-repeat-h",
|
||||
"name": "Library Panel with Horizontal Repeat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Library Panel with Vertical Repeat",
|
||||
"type": "library-panel-ref",
|
||||
"gridPos": {
|
||||
"x": 0,
|
||||
"y": 8,
|
||||
"w": 6,
|
||||
"h": 4
|
||||
},
|
||||
"libraryPanel": {
|
||||
"uid": "lib-panel-repeat-v",
|
||||
"name": "Library Panel with Vertical Repeat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Library Panel Instance Override",
|
||||
"type": "library-panel-ref",
|
||||
"gridPos": {
|
||||
"x": 6,
|
||||
"y": 8,
|
||||
"w": 12,
|
||||
"h": 8
|
||||
},
|
||||
"libraryPanel": {
|
||||
"uid": "lib-panel-repeat-h",
|
||||
"name": "Library Panel with Horizontal Repeat"
|
||||
},
|
||||
"repeat": "instance-var",
|
||||
"repeatDirection": "v",
|
||||
"maxPerRow": 5
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Library Panel without Repeat",
|
||||
"type": "library-panel-ref",
|
||||
"gridPos": {
|
||||
"x": 0,
|
||||
"y": 12,
|
||||
"w": 6,
|
||||
"h": 3
|
||||
},
|
||||
"libraryPanel": {
|
||||
"uid": "lib-panel-no-repeat",
|
||||
"name": "Library Panel without Repeat"
|
||||
}
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"links": []
|
||||
}
|
||||
}
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"kind": "Dashboard",
|
||||
"apiVersion": "dashboard.grafana.app/v0alpha1",
|
||||
"metadata": {
|
||||
"name": "library-panel-repeat-options-test",
|
||||
"labels": {
|
||||
"test": "library-panel-repeat"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"description": "Testing library panel repeat options migration from v1beta1 to v2alpha1",
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"libraryPanel": {
|
||||
"name": "Library Panel with Horizontal Repeat",
|
||||
"uid": "lib-panel-repeat-h"
|
||||
},
|
||||
"title": "Library Panel with Horizontal Repeat",
|
||||
"type": "library-panel-ref"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 2,
|
||||
"libraryPanel": {
|
||||
"name": "Library Panel with Vertical Repeat",
|
||||
"uid": "lib-panel-repeat-v"
|
||||
},
|
||||
"title": "Library Panel with Vertical Repeat",
|
||||
"type": "library-panel-ref"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 6,
|
||||
"y": 8
|
||||
},
|
||||
"id": 3,
|
||||
"libraryPanel": {
|
||||
"name": "Library Panel with Horizontal Repeat",
|
||||
"uid": "lib-panel-repeat-h"
|
||||
},
|
||||
"maxPerRow": 5,
|
||||
"repeat": "instance-var",
|
||||
"repeatDirection": "v",
|
||||
"title": "Library Panel Instance Override",
|
||||
"type": "library-panel-ref"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 3,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 12
|
||||
},
|
||||
"id": 4,
|
||||
"libraryPanel": {
|
||||
"name": "Library Panel without Repeat",
|
||||
"uid": "lib-panel-no-repeat"
|
||||
},
|
||||
"title": "Library Panel without Repeat",
|
||||
"type": "library-panel-ref"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 38,
|
||||
"tags": [
|
||||
"test",
|
||||
"library-panels",
|
||||
"repeat"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"title": "Library Panel Repeat Options Test Dashboard"
|
||||
},
|
||||
"status": {
|
||||
"conversion": {
|
||||
"failed": false,
|
||||
"storedVersion": "v1beta1"
|
||||
}
|
||||
}
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"kind": "Dashboard",
|
||||
"apiVersion": "dashboard.grafana.app/v2alpha1",
|
||||
"metadata": {
|
||||
"name": "library-panel-repeat-options-test",
|
||||
"labels": {
|
||||
"test": "library-panel-repeat"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"annotations": [],
|
||||
"cursorSync": "Off",
|
||||
"description": "Testing library panel repeat options migration from v1beta1 to v2alpha1",
|
||||
"editable": true,
|
||||
"elements": {
|
||||
"panel-1": {
|
||||
"kind": "LibraryPanel",
|
||||
"spec": {
|
||||
"id": 1,
|
||||
"title": "Library Panel with Horizontal Repeat",
|
||||
"libraryPanel": {
|
||||
"name": "Library Panel with Horizontal Repeat",
|
||||
"uid": "lib-panel-repeat-h"
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-2": {
|
||||
"kind": "LibraryPanel",
|
||||
"spec": {
|
||||
"id": 2,
|
||||
"title": "Library Panel with Vertical Repeat",
|
||||
"libraryPanel": {
|
||||
"name": "Library Panel with Vertical Repeat",
|
||||
"uid": "lib-panel-repeat-v"
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-3": {
|
||||
"kind": "LibraryPanel",
|
||||
"spec": {
|
||||
"id": 3,
|
||||
"title": "Library Panel Instance Override",
|
||||
"libraryPanel": {
|
||||
"name": "Library Panel with Horizontal Repeat",
|
||||
"uid": "lib-panel-repeat-h"
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-4": {
|
||||
"kind": "LibraryPanel",
|
||||
"spec": {
|
||||
"id": 4,
|
||||
"title": "Library Panel without Repeat",
|
||||
"libraryPanel": {
|
||||
"name": "Library Panel without Repeat",
|
||||
"uid": "lib-panel-no-repeat"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"kind": "GridLayout",
|
||||
"spec": {
|
||||
"items": [
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 12,
|
||||
"height": 8,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-1"
|
||||
},
|
||||
"repeat": {
|
||||
"mode": "variable",
|
||||
"value": "server",
|
||||
"direction": "h",
|
||||
"maxPerRow": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 0,
|
||||
"y": 8,
|
||||
"width": 6,
|
||||
"height": 4,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-2"
|
||||
},
|
||||
"repeat": {
|
||||
"mode": "variable",
|
||||
"value": "datacenter",
|
||||
"direction": "v"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 6,
|
||||
"y": 8,
|
||||
"width": 12,
|
||||
"height": 8,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-3"
|
||||
},
|
||||
"repeat": {
|
||||
"mode": "variable",
|
||||
"value": "instance-var",
|
||||
"direction": "v",
|
||||
"maxPerRow": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 0,
|
||||
"y": 12,
|
||||
"width": 6,
|
||||
"height": 3,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-4"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"preload": false,
|
||||
"tags": [
|
||||
"test",
|
||||
"library-panels",
|
||||
"repeat"
|
||||
],
|
||||
"timeSettings": {
|
||||
"timezone": "browser",
|
||||
"from": "now-1h",
|
||||
"to": "now",
|
||||
"autoRefresh": "",
|
||||
"autoRefreshIntervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"hideTimepicker": false,
|
||||
"fiscalYearStartMonth": 0
|
||||
},
|
||||
"title": "Library Panel Repeat Options Test Dashboard",
|
||||
"variables": []
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"kind": "Dashboard",
|
||||
"apiVersion": "dashboard.grafana.app/v2beta1",
|
||||
"metadata": {
|
||||
"name": "library-panel-repeat-options-test",
|
||||
"labels": {
|
||||
"test": "library-panel-repeat"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"annotations": [],
|
||||
"cursorSync": "Off",
|
||||
"description": "Testing library panel repeat options migration from v1beta1 to v2alpha1",
|
||||
"editable": true,
|
||||
"elements": {
|
||||
"panel-1": {
|
||||
"kind": "LibraryPanel",
|
||||
"spec": {
|
||||
"id": 1,
|
||||
"title": "Library Panel with Horizontal Repeat",
|
||||
"libraryPanel": {
|
||||
"name": "Library Panel with Horizontal Repeat",
|
||||
"uid": "lib-panel-repeat-h"
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-2": {
|
||||
"kind": "LibraryPanel",
|
||||
"spec": {
|
||||
"id": 2,
|
||||
"title": "Library Panel with Vertical Repeat",
|
||||
"libraryPanel": {
|
||||
"name": "Library Panel with Vertical Repeat",
|
||||
"uid": "lib-panel-repeat-v"
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-3": {
|
||||
"kind": "LibraryPanel",
|
||||
"spec": {
|
||||
"id": 3,
|
||||
"title": "Library Panel Instance Override",
|
||||
"libraryPanel": {
|
||||
"name": "Library Panel with Horizontal Repeat",
|
||||
"uid": "lib-panel-repeat-h"
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-4": {
|
||||
"kind": "LibraryPanel",
|
||||
"spec": {
|
||||
"id": 4,
|
||||
"title": "Library Panel without Repeat",
|
||||
"libraryPanel": {
|
||||
"name": "Library Panel without Repeat",
|
||||
"uid": "lib-panel-no-repeat"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"kind": "GridLayout",
|
||||
"spec": {
|
||||
"items": [
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 12,
|
||||
"height": 8,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-1"
|
||||
},
|
||||
"repeat": {
|
||||
"mode": "variable",
|
||||
"value": "server",
|
||||
"direction": "h",
|
||||
"maxPerRow": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 0,
|
||||
"y": 8,
|
||||
"width": 6,
|
||||
"height": 4,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-2"
|
||||
},
|
||||
"repeat": {
|
||||
"mode": "variable",
|
||||
"value": "datacenter",
|
||||
"direction": "v"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 6,
|
||||
"y": 8,
|
||||
"width": 12,
|
||||
"height": 8,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-3"
|
||||
},
|
||||
"repeat": {
|
||||
"mode": "variable",
|
||||
"value": "instance-var",
|
||||
"direction": "v",
|
||||
"maxPerRow": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 0,
|
||||
"y": 12,
|
||||
"width": 6,
|
||||
"height": 3,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-4"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"preload": false,
|
||||
"tags": [
|
||||
"test",
|
||||
"library-panels",
|
||||
"repeat"
|
||||
],
|
||||
"timeSettings": {
|
||||
"timezone": "browser",
|
||||
"from": "now-1h",
|
||||
"to": "now",
|
||||
"autoRefresh": "",
|
||||
"autoRefreshIntervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"hideTimepicker": false,
|
||||
"fiscalYearStartMonth": 0
|
||||
},
|
||||
"title": "Library Panel Repeat Options Test Dashboard",
|
||||
"variables": []
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ func Convert_V0_to_V1beta1(in *dashv0.Dashboard, out *dashv1.Dashboard, scope co
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error {
|
||||
func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error {
|
||||
v1beta1 := &dashv1.Dashboard{}
|
||||
if err := ConvertDashboard_V0_to_V1beta1(in, v1beta1, scope); err != nil {
|
||||
out.Status = dashv2alpha1.DashboardStatus{
|
||||
@@ -48,7 +48,7 @@ func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, s
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := ConvertDashboard_V1beta1_to_V2alpha1(v1beta1, out, scope, dsIndexProvider); err != nil {
|
||||
if err := ConvertDashboard_V1beta1_to_V2alpha1(v1beta1, out, scope, dsIndexProvider, leIndexProvider); err != nil {
|
||||
out.Status = dashv2alpha1.DashboardStatus{
|
||||
Conversion: &dashv2alpha1.DashboardConversionStatus{
|
||||
StoredVersion: ptr.To(dashv0.VERSION),
|
||||
@@ -72,7 +72,7 @@ func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, s
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error {
|
||||
func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error {
|
||||
v1beta1 := &dashv1.Dashboard{}
|
||||
if err := ConvertDashboard_V0_to_V1beta1(in, v1beta1, scope); err != nil {
|
||||
out.Status = dashv2beta1.DashboardStatus{
|
||||
@@ -86,7 +86,7 @@ func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, sco
|
||||
}
|
||||
|
||||
v2alpha1 := &dashv2alpha1.Dashboard{}
|
||||
if err := ConvertDashboard_V1beta1_to_V2alpha1(v1beta1, v2alpha1, scope, dsIndexProvider); err != nil {
|
||||
if err := ConvertDashboard_V1beta1_to_V2alpha1(v1beta1, v2alpha1, scope, dsIndexProvider, leIndexProvider); err != nil {
|
||||
out.Status = dashv2beta1.DashboardStatus{
|
||||
Conversion: &dashv2beta1.DashboardConversionStatus{
|
||||
StoredVersion: ptr.To(dashv0.VERSION),
|
||||
|
||||
@@ -109,9 +109,9 @@ func TestV0ConversionErrorHandling(t *testing.T) {
|
||||
case *dashv1.Dashboard:
|
||||
err = Convert_V0_to_V1beta1(tt.source, target, nil)
|
||||
case *dashv2alpha1.Dashboard:
|
||||
err = Convert_V0_to_V2alpha1(tt.source, target, nil, dsProvider)
|
||||
err = Convert_V0_to_V2alpha1(tt.source, target, nil, dsProvider, leProvider)
|
||||
case *dashv2beta1.Dashboard:
|
||||
err = Convert_V0_to_V2beta1(tt.source, target, nil, dsProvider)
|
||||
err = Convert_V0_to_V2beta1(tt.source, target, nil, dsProvider, leProvider)
|
||||
default:
|
||||
t.Fatalf("unexpected target type: %T", target)
|
||||
}
|
||||
@@ -192,7 +192,7 @@ func TestV0ConversionErrorPropagation(t *testing.T) {
|
||||
}
|
||||
target := &dashv2beta1.Dashboard{}
|
||||
|
||||
err := Convert_V0_to_V2beta1(source, target, nil, dsProvider)
|
||||
err := Convert_V0_to_V2beta1(source, target, nil, dsProvider, leProvider)
|
||||
|
||||
require.Error(t, err, "expected error to be returned on first step failure")
|
||||
require.NotNil(t, target.Status.Conversion)
|
||||
@@ -243,7 +243,7 @@ func TestV0ConversionSuccessPaths(t *testing.T) {
|
||||
}
|
||||
target := &dashv2alpha1.Dashboard{}
|
||||
|
||||
err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider)
|
||||
err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider, leProvider)
|
||||
|
||||
require.NoError(t, err, "expected successful conversion")
|
||||
// Layout should be set even on success
|
||||
@@ -264,7 +264,7 @@ func TestV0ConversionSuccessPaths(t *testing.T) {
|
||||
}
|
||||
target := &dashv2beta1.Dashboard{}
|
||||
|
||||
err := Convert_V0_to_V2beta1(source, target, nil, dsProvider)
|
||||
err := Convert_V0_to_V2beta1(source, target, nil, dsProvider, leProvider)
|
||||
|
||||
require.NoError(t, err, "expected successful conversion")
|
||||
})
|
||||
@@ -293,7 +293,7 @@ func TestV0ConversionSecondStepErrors(t *testing.T) {
|
||||
}
|
||||
target := &dashv2alpha1.Dashboard{}
|
||||
|
||||
err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider)
|
||||
err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider, leProvider)
|
||||
|
||||
// Convert_V0_to_V2alpha1 doesn't return error, just sets status
|
||||
require.NoError(t, err, "Convert_V0_to_V2alpha1 doesn't return error")
|
||||
@@ -327,7 +327,7 @@ func TestV0ConversionSecondStepErrors(t *testing.T) {
|
||||
}
|
||||
target := &dashv2alpha1.Dashboard{}
|
||||
|
||||
err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider)
|
||||
err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider, leProvider)
|
||||
|
||||
// Convert_V0_to_V2alpha1 doesn't return error, just sets status
|
||||
require.NoError(t, err, "Convert_V0_to_V2alpha1 doesn't return error")
|
||||
@@ -357,7 +357,7 @@ func TestV0ConversionSecondStepErrors(t *testing.T) {
|
||||
}
|
||||
target := &dashv2beta1.Dashboard{}
|
||||
|
||||
err := Convert_V0_to_V2beta1(source, target, nil, dsProvider)
|
||||
err := Convert_V0_to_V2beta1(source, target, nil, dsProvider, leProvider)
|
||||
|
||||
// May or may not error depending on dashboard content
|
||||
// But if it does error on second step, status should be set
|
||||
@@ -383,7 +383,7 @@ func TestV0ConversionSecondStepErrors(t *testing.T) {
|
||||
}
|
||||
target := &dashv2beta1.Dashboard{}
|
||||
|
||||
err := Convert_V0_to_V2beta1(source, target, nil, dsProvider)
|
||||
err := Convert_V0_to_V2beta1(source, target, nil, dsProvider, leProvider)
|
||||
|
||||
// May or may not error depending on dashboard content
|
||||
// But if it does error on third step, status should be set
|
||||
|
||||
@@ -25,8 +25,8 @@ func Convert_V1beta1_to_V0(in *dashv1.Dashboard, out *dashv0.Dashboard, scope co
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error {
|
||||
if err := ConvertDashboard_V1beta1_to_V2alpha1(in, out, scope, dsIndexProvider); err != nil {
|
||||
func Convert_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error {
|
||||
if err := ConvertDashboard_V1beta1_to_V2alpha1(in, out, scope, dsIndexProvider, leIndexProvider); err != nil {
|
||||
out.Status = dashv2alpha1.DashboardStatus{
|
||||
Conversion: &dashv2alpha1.DashboardConversionStatus{
|
||||
StoredVersion: ptr.To(dashv1.VERSION),
|
||||
@@ -60,9 +60,9 @@ func Convert_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboa
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_V1beta1_to_V2beta1(in *dashv1.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error {
|
||||
func Convert_V1beta1_to_V2beta1(in *dashv1.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error {
|
||||
v2alpha1 := &dashv2alpha1.Dashboard{}
|
||||
if err := ConvertDashboard_V1beta1_to_V2alpha1(in, v2alpha1, scope, dsIndexProvider); err != nil {
|
||||
if err := ConvertDashboard_V1beta1_to_V2alpha1(in, v2alpha1, scope, dsIndexProvider, leIndexProvider); err != nil {
|
||||
out.Status = dashv2beta1.DashboardStatus{
|
||||
Conversion: &dashv2beta1.DashboardConversionStatus{
|
||||
StoredVersion: ptr.To(dashv1.VERSION),
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestV1ConversionErrorHandling(t *testing.T) {
|
||||
}
|
||||
target := &dashv2alpha1.Dashboard{}
|
||||
|
||||
err := Convert_V1beta1_to_V2alpha1(source, target, nil, dsProvider)
|
||||
err := Convert_V1beta1_to_V2alpha1(source, target, nil, dsProvider, leProvider)
|
||||
|
||||
// Convert_V1beta1_to_V2alpha1 doesn't return error, just sets status
|
||||
require.NoError(t, err, "Convert_V1beta1_to_V2alpha1 doesn't return error")
|
||||
@@ -64,7 +64,7 @@ func TestV1ConversionErrorHandling(t *testing.T) {
|
||||
}
|
||||
target := &dashv2beta1.Dashboard{}
|
||||
|
||||
err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider)
|
||||
err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider, leProvider)
|
||||
|
||||
// May or may not error depending on dashboard content
|
||||
// But if it does error on first step, status should be set with correct StoredVersion
|
||||
@@ -91,7 +91,7 @@ func TestV1ConversionErrorHandling(t *testing.T) {
|
||||
}
|
||||
target := &dashv2beta1.Dashboard{}
|
||||
|
||||
err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider)
|
||||
err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider, leProvider)
|
||||
|
||||
// May or may not error depending on dashboard content
|
||||
// But if it does error on second step, status should be set with correct StoredVersion
|
||||
@@ -117,7 +117,7 @@ func TestV1ConversionErrorHandling(t *testing.T) {
|
||||
}
|
||||
target := &dashv2beta1.Dashboard{}
|
||||
|
||||
err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider)
|
||||
err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider, leProvider)
|
||||
|
||||
// Should succeed if dashboard is valid
|
||||
if err == nil {
|
||||
|
||||
@@ -80,7 +80,7 @@ func prepareV1beta1ConversionContext(in *dashv1.Dashboard, dsIndexProvider schem
|
||||
return ctx, &nsInfo, nil
|
||||
}
|
||||
|
||||
func ConvertDashboard_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error {
|
||||
func ConvertDashboard_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error {
|
||||
out.ObjectMeta = in.ObjectMeta
|
||||
out.APIVersion = dashv2alpha1.APIVERSION
|
||||
out.Kind = in.Kind
|
||||
@@ -94,10 +94,10 @@ func ConvertDashboard_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha
|
||||
return fmt.Errorf("failed to prepare conversion context: %w", err)
|
||||
}
|
||||
|
||||
return convertDashboardSpec_V1beta1_to_V2alpha1(&in.Spec, &out.Spec, scope, ctx, dsIndexProvider)
|
||||
return convertDashboardSpec_V1beta1_to_V2alpha1(&in.Spec, &out.Spec, scope, ctx, dsIndexProvider, leIndexProvider)
|
||||
}
|
||||
|
||||
func convertDashboardSpec_V1beta1_to_V2alpha1(in *dashv1.DashboardSpec, out *dashv2alpha1.DashboardSpec, scope conversion.Scope, ctx context.Context, dsIndexProvider schemaversion.DataSourceIndexProvider) error {
|
||||
func convertDashboardSpec_V1beta1_to_V2alpha1(in *dashv1.DashboardSpec, out *dashv2alpha1.DashboardSpec, scope conversion.Scope, ctx context.Context, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error {
|
||||
// Parse the unstructured spec into a dashboard JSON structure
|
||||
dashboardJSON, ok := in.Object["dashboard"]
|
||||
if !ok {
|
||||
@@ -161,7 +161,7 @@ func convertDashboardSpec_V1beta1_to_V2alpha1(in *dashv1.DashboardSpec, out *das
|
||||
out.Links = transformLinks(dashboard)
|
||||
|
||||
// Transform panels to elements and layout
|
||||
elements, layout, err := transformPanelsToElementsAndLayout(ctx, dashboard, dsIndexProvider)
|
||||
elements, layout, err := transformPanelsToElementsAndLayout(ctx, dashboard, dsIndexProvider, leIndexProvider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to transform panels: %w", err)
|
||||
}
|
||||
@@ -387,7 +387,7 @@ func transformLinks(dashboard map[string]interface{}) []dashv2alpha1.DashboardDa
|
||||
// Panel transformation constants
|
||||
const GRID_ROW_HEIGHT = 1
|
||||
|
||||
func transformPanelsToElementsAndLayout(ctx context.Context, dashboard map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) {
|
||||
func transformPanelsToElementsAndLayout(ctx context.Context, dashboard map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) {
|
||||
panels, ok := dashboard["panels"].([]interface{})
|
||||
if !ok {
|
||||
// Return empty elements and default grid layout
|
||||
@@ -415,13 +415,13 @@ func transformPanelsToElementsAndLayout(ctx context.Context, dashboard map[strin
|
||||
}
|
||||
|
||||
if hasRowPanels {
|
||||
return convertToRowsLayout(ctx, panels, dsIndexProvider)
|
||||
return convertToRowsLayout(ctx, panels, dsIndexProvider, leIndexProvider)
|
||||
}
|
||||
|
||||
return convertToGridLayout(ctx, panels, dsIndexProvider)
|
||||
return convertToGridLayout(ctx, panels, dsIndexProvider, leIndexProvider)
|
||||
}
|
||||
|
||||
func convertToGridLayout(ctx context.Context, panels []interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) {
|
||||
func convertToGridLayout(ctx context.Context, panels []interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) {
|
||||
elements := make(map[string]dashv2alpha1.DashboardElement)
|
||||
items := make([]dashv2alpha1.DashboardGridLayoutItemKind, 0, len(panels))
|
||||
|
||||
@@ -437,7 +437,7 @@ func convertToGridLayout(ctx context.Context, panels []interface{}, dsIndexProvi
|
||||
}
|
||||
|
||||
elements[elementName] = element
|
||||
items = append(items, buildGridItemKind(panelMap, elementName, nil))
|
||||
items = append(items, buildGridItemKind(ctx, panelMap, elementName, nil, leIndexProvider))
|
||||
}
|
||||
|
||||
layout := dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{
|
||||
@@ -452,7 +452,7 @@ func convertToGridLayout(ctx context.Context, panels []interface{}, dsIndexProvi
|
||||
return elements, layout, nil
|
||||
}
|
||||
|
||||
func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) {
|
||||
func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) {
|
||||
elements := make(map[string]dashv2alpha1.DashboardElement)
|
||||
rows := make([]dashv2alpha1.DashboardRowsLayoutRowKind, 0)
|
||||
|
||||
@@ -491,7 +491,7 @@ func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvi
|
||||
element, name, err := buildElement(ctx, collapsedPanelMap, dsIndexProvider)
|
||||
if err == nil {
|
||||
elements[name] = element
|
||||
rowElements = append(rowElements, buildGridItemKind(collapsedPanelMap, name, int64Ptr(yOffsetInRows(collapsedPanelMap, legacyRowY))))
|
||||
rowElements = append(rowElements, buildGridItemKind(ctx, collapsedPanelMap, name, int64Ptr(yOffsetInRows(collapsedPanelMap, legacyRowY)), leIndexProvider))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -512,7 +512,7 @@ func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvi
|
||||
if currentRow.Spec.Layout.GridLayoutKind != nil {
|
||||
currentRow.Spec.Layout.GridLayoutKind.Spec.Items = append(
|
||||
currentRow.Spec.Layout.GridLayoutKind.Spec.Items,
|
||||
buildGridItemKind(panelMap, elementName, int64Ptr(yOffsetInRows(panelMap, legacyRowY))),
|
||||
buildGridItemKind(ctx, panelMap, elementName, int64Ptr(yOffsetInRows(panelMap, legacyRowY)), leIndexProvider),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -521,7 +521,7 @@ func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvi
|
||||
// The Y position does not matter for the rows layout, but it's used to calculate the position of the panels in the grid layout in the row.
|
||||
legacyRowY = -1
|
||||
gridItems := []dashv2alpha1.DashboardGridLayoutItemKind{
|
||||
buildGridItemKind(panelMap, elementName, int64Ptr(0)),
|
||||
buildGridItemKind(ctx, panelMap, elementName, int64Ptr(0), leIndexProvider),
|
||||
}
|
||||
|
||||
hideHeader := true
|
||||
@@ -645,7 +645,7 @@ func buildPanelKind(ctx context.Context, panelMap map[string]interface{}, dsInde
|
||||
return panelKind, nil
|
||||
}
|
||||
|
||||
func buildGridItemKind(panelMap map[string]interface{}, elementName string, yOverride *int64) dashv2alpha1.DashboardGridLayoutItemKind {
|
||||
func buildGridItemKind(ctx context.Context, panelMap map[string]interface{}, elementName string, yOverride *int64, leIndexProvider schemaversion.LibraryElementIndexProvider) dashv2alpha1.DashboardGridLayoutItemKind {
|
||||
// Default grid position (matches frontend PanelModel defaults: w=6, h=3)
|
||||
x, y, width, height := int64(0), int64(0), int64(6), int64(3)
|
||||
|
||||
@@ -677,34 +677,78 @@ func buildGridItemKind(panelMap map[string]interface{}, elementName string, yOve
|
||||
}
|
||||
|
||||
// Handle repeat options
|
||||
if repeat := schemaversion.GetStringValue(panelMap, "repeat"); repeat != "" {
|
||||
repeatOptions := &dashv2alpha1.DashboardRepeatOptions{
|
||||
Mode: "variable",
|
||||
Value: repeat,
|
||||
}
|
||||
// First check if repeat options are set on the panel itself (dashboard instance level)
|
||||
repeatOptions := getRepeatOptionsFromPanel(panelMap)
|
||||
|
||||
if repeatDirection := schemaversion.GetStringValue(panelMap, "repeatDirection"); repeatDirection != "" {
|
||||
switch repeatDirection {
|
||||
case "h":
|
||||
direction := dashv2alpha1.DashboardRepeatOptionsDirectionH
|
||||
repeatOptions.Direction = &direction
|
||||
case "v":
|
||||
direction := dashv2alpha1.DashboardRepeatOptionsDirectionV
|
||||
repeatOptions.Direction = &direction
|
||||
// If no repeat options on the panel and it's a library panel, try to get them from the library panel definition
|
||||
if repeatOptions == nil {
|
||||
if libraryPanel, ok := panelMap["libraryPanel"].(map[string]interface{}); ok {
|
||||
libraryPanelUID := schemaversion.GetStringValue(libraryPanel, "uid")
|
||||
if libraryPanelUID != "" && leIndexProvider != nil {
|
||||
repeatOptions = getRepeatOptionsFromLibraryPanel(ctx, libraryPanelUID, leIndexProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if maxPerRow := getIntField(panelMap, "maxPerRow", 0); maxPerRow > 0 {
|
||||
maxPerRowInt64 := int64(maxPerRow)
|
||||
repeatOptions.MaxPerRow = &maxPerRowInt64
|
||||
}
|
||||
|
||||
if repeatOptions != nil {
|
||||
item.Spec.Repeat = repeatOptions
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
// getRepeatOptionsFromPanel extracts repeat options from a panel map (dashboard instance level)
|
||||
func getRepeatOptionsFromPanel(panelMap map[string]any) *dashv2alpha1.DashboardRepeatOptions {
|
||||
repeat := schemaversion.GetStringValue(panelMap, "repeat")
|
||||
if repeat == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
repeatOptions := &dashv2alpha1.DashboardRepeatOptions{
|
||||
Mode: "variable",
|
||||
Value: repeat,
|
||||
}
|
||||
|
||||
if repeatDirection := schemaversion.GetStringValue(panelMap, "repeatDirection"); repeatDirection != "" {
|
||||
switch repeatDirection {
|
||||
case "h":
|
||||
direction := dashv2alpha1.DashboardRepeatOptionsDirectionH
|
||||
repeatOptions.Direction = &direction
|
||||
case "v":
|
||||
direction := dashv2alpha1.DashboardRepeatOptionsDirectionV
|
||||
repeatOptions.Direction = &direction
|
||||
}
|
||||
}
|
||||
|
||||
if maxPerRow := getIntField(panelMap, "maxPerRow", 0); maxPerRow > 0 {
|
||||
maxPerRowInt64 := int64(maxPerRow)
|
||||
repeatOptions.MaxPerRow = &maxPerRowInt64
|
||||
}
|
||||
|
||||
return repeatOptions
|
||||
}
|
||||
|
||||
// getRepeatOptionsFromLibraryPanel retrieves repeat options from a library panel by UID
|
||||
func getRepeatOptionsFromLibraryPanel(ctx context.Context, libraryPanelUID string, leIndexProvider schemaversion.LibraryElementIndexProvider) *dashv2alpha1.DashboardRepeatOptions {
|
||||
libraryElements := leIndexProvider.GetLibraryElementInfo(ctx)
|
||||
|
||||
// Find the library panel by UID
|
||||
var libraryPanelModel map[string]any
|
||||
for _, elem := range libraryElements {
|
||||
if elem.UID == libraryPanelUID {
|
||||
libraryPanelModel = elem.Model.Object
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if libraryPanelModel == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extract repeat options from the library panel model
|
||||
return getRepeatOptionsFromPanel(libraryPanelModel)
|
||||
}
|
||||
|
||||
func buildRowKind(rowPanelMap map[string]interface{}, elements []dashv2alpha1.DashboardGridLayoutItemKind) *dashv2alpha1.DashboardRowsLayoutRowKind {
|
||||
collapsed := getBoolField(rowPanelMap, "collapsed", false)
|
||||
title := schemaversion.GetStringValue(rowPanelMap, "title")
|
||||
|
||||
@@ -216,3 +216,60 @@ func MigrateDatasourceNameToRef(nameOrRef interface{}, options map[string]bool,
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cachedLibraryElementProvider wraps a LibraryElementIndexProvider with time-based caching.
|
||||
// This prevents multiple DB queries during operations that may call GetLibraryElementInfo()
|
||||
// multiple times (e.g., dashboard conversions with many library panel lookups).
|
||||
// The cache expires after 10 seconds, allowing it to be used as a long-lived singleton
|
||||
// while still refreshing periodically.
|
||||
//
|
||||
// Thread-safe: Uses sync.RWMutex to guarantee safe concurrent access.
|
||||
type cachedLibraryElementProvider struct {
|
||||
provider LibraryElementIndexProvider
|
||||
mu sync.RWMutex
|
||||
elements []LibraryElementInfo
|
||||
cachedAt time.Time
|
||||
cacheTTL time.Duration
|
||||
}
|
||||
|
||||
// GetLibraryElementInfo returns the cached library elements if they're still valid (< 10s old), otherwise rebuilds the cache.
|
||||
// Uses RWMutex for efficient concurrent reads when cache is valid.
|
||||
func (p *cachedLibraryElementProvider) GetLibraryElementInfo(ctx context.Context) []LibraryElementInfo {
|
||||
// Fast path: check if cache is still valid using read lock
|
||||
p.mu.RLock()
|
||||
if p.elements != nil && time.Since(p.cachedAt) < p.cacheTTL {
|
||||
elements := p.elements
|
||||
p.mu.RUnlock()
|
||||
return elements
|
||||
}
|
||||
p.mu.RUnlock()
|
||||
|
||||
// Slow path: cache expired or not yet built, acquire write lock
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
// Double-check: another goroutine might have refreshed the cache
|
||||
// while we were waiting for the write lock
|
||||
if p.elements != nil && time.Since(p.cachedAt) < p.cacheTTL {
|
||||
return p.elements
|
||||
}
|
||||
|
||||
// Rebuild the cache
|
||||
p.elements = p.provider.GetLibraryElementInfo(ctx)
|
||||
p.cachedAt = time.Now()
|
||||
return p.elements
|
||||
}
|
||||
|
||||
// WrapLibraryElementProviderWithCache wraps a provider to cache library elements with a 10-second TTL.
|
||||
// Useful for conversions or migrations that may call GetLibraryElementInfo() multiple times.
|
||||
// The cache expires after 10 seconds, making it suitable for use as a long-lived singleton
|
||||
// at the top level of dependency injection while still refreshing periodically.
|
||||
func WrapLibraryElementProviderWithCache(provider LibraryElementIndexProvider) LibraryElementIndexProvider {
|
||||
if provider == nil {
|
||||
return nil
|
||||
}
|
||||
return &cachedLibraryElementProvider{
|
||||
provider: provider,
|
||||
cacheTTL: 10 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package schemaversion
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -34,6 +36,7 @@ type LibraryElementInfo struct {
|
||||
Type string
|
||||
Description string
|
||||
FolderUID string
|
||||
Model common.Unstructured // JSON model of the library element, used to extract repeat options during migration
|
||||
}
|
||||
|
||||
type LibraryElementIndexProvider interface {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
)
|
||||
|
||||
// EmptyLibraryElementProvider provides an empty library element list for tests
|
||||
@@ -187,3 +188,101 @@ func (p *ConfigurableDataSourceProvider) getDevDashboardDataSources() []schemave
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TestLibraryElementProvider provides library elements with models for testing repeat options migration
|
||||
type TestLibraryElementProvider struct {
|
||||
elements []schemaversion.LibraryElementInfo
|
||||
}
|
||||
|
||||
// NewTestLibraryElementProvider creates a new test library element provider with sample library panels
|
||||
func NewTestLibraryElementProvider() *TestLibraryElementProvider {
|
||||
// Create library panel models with repeat options
|
||||
libPanelWithRepeatH := map[string]any{
|
||||
"id": 1,
|
||||
"type": "timeseries",
|
||||
"title": "Library Panel with Horizontal Repeat",
|
||||
"repeat": "server",
|
||||
"repeatDirection": "h",
|
||||
"maxPerRow": 3,
|
||||
"gridPos": map[string]any{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 12,
|
||||
"h": 8,
|
||||
},
|
||||
"targets": []any{},
|
||||
"options": map[string]any{},
|
||||
}
|
||||
|
||||
libPanelWithRepeatV := map[string]any{
|
||||
"id": 2,
|
||||
"type": "stat",
|
||||
"title": "Library Panel with Vertical Repeat",
|
||||
"repeat": "datacenter",
|
||||
"repeatDirection": "v",
|
||||
"gridPos": map[string]any{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 4,
|
||||
},
|
||||
"targets": []any{},
|
||||
"options": map[string]any{},
|
||||
}
|
||||
|
||||
libPanelWithoutRepeat := map[string]any{
|
||||
"id": 3,
|
||||
"type": "text",
|
||||
"title": "Library Panel without Repeat",
|
||||
"gridPos": map[string]any{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 3,
|
||||
},
|
||||
"targets": []any{},
|
||||
"options": map[string]any{},
|
||||
}
|
||||
|
||||
// Convert models to Unstructured
|
||||
modelWithRepeatH := v0alpha1.Unstructured{Object: libPanelWithRepeatH}
|
||||
modelWithRepeatV := v0alpha1.Unstructured{Object: libPanelWithRepeatV}
|
||||
modelWithoutRepeat := v0alpha1.Unstructured{Object: libPanelWithoutRepeat}
|
||||
|
||||
return &TestLibraryElementProvider{
|
||||
elements: []schemaversion.LibraryElementInfo{
|
||||
{
|
||||
UID: "lib-panel-repeat-h",
|
||||
Name: "Library Panel with Horizontal Repeat",
|
||||
Kind: 1, // Panel element
|
||||
Type: "timeseries",
|
||||
Description: "A library panel with horizontal repeat options",
|
||||
FolderUID: "",
|
||||
Model: modelWithRepeatH,
|
||||
},
|
||||
{
|
||||
UID: "lib-panel-repeat-v",
|
||||
Name: "Library Panel with Vertical Repeat",
|
||||
Kind: 1, // Panel element
|
||||
Type: "stat",
|
||||
Description: "A library panel with vertical repeat options",
|
||||
FolderUID: "",
|
||||
Model: modelWithRepeatV,
|
||||
},
|
||||
{
|
||||
UID: "lib-panel-no-repeat",
|
||||
Name: "Library Panel without Repeat",
|
||||
Kind: 1, // Panel element
|
||||
Type: "text",
|
||||
Description: "A library panel without repeat options",
|
||||
FolderUID: "",
|
||||
Model: modelWithoutRepeat,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetLibraryElementInfo returns the test library elements
|
||||
func (p *TestLibraryElementProvider) GetLibraryElementInfo(_ context.Context) []schemaversion.LibraryElementInfo {
|
||||
return p.elements
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
@@ -112,6 +114,13 @@ func (l *libraryElementIndexProvider) GetLibraryElementInfo(ctx context.Context)
|
||||
}
|
||||
|
||||
for _, elem := range result.Elements {
|
||||
var modelUnstructured v0alpha1.Unstructured
|
||||
if len(elem.Model) > 0 {
|
||||
var modelObj map[string]any
|
||||
if err := json.Unmarshal(elem.Model, &modelObj); err == nil {
|
||||
modelUnstructured.Object = modelObj
|
||||
}
|
||||
}
|
||||
info = append(info, schemaversion.LibraryElementInfo{
|
||||
UID: elem.UID,
|
||||
Name: elem.Name,
|
||||
@@ -119,6 +128,7 @@ func (l *libraryElementIndexProvider) GetLibraryElementInfo(ctx context.Context)
|
||||
Type: elem.Type,
|
||||
Description: elem.Description,
|
||||
FolderUID: elem.FolderUID,
|
||||
Model: modelUnstructured,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
import { getLibraryPanel } from 'app/features/library-panels/state/api';
|
||||
|
||||
import { createPanelDataProvider } from '../utils/createPanelDataProvider';
|
||||
import { getPanelIdForVizPanel } from '../utils/utils';
|
||||
import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils';
|
||||
|
||||
import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks';
|
||||
import { panelLinksBehavior } from './PanelMenuBehavior';
|
||||
@@ -99,8 +99,19 @@ export class LibraryPanelBehavior extends SceneObjectBase<LibraryPanelBehaviorSt
|
||||
|
||||
const layoutElement = vizPanel.parent!;
|
||||
|
||||
// Migrate repeat options to layout element
|
||||
if (libPanelModel.repeat && layoutElement instanceof DashboardGridItem) {
|
||||
// Skip migrating repeat options from library panel when using dynamic dashboards (dashboardNewLayouts),
|
||||
// as repeat options should only come from the dashboard panel instance (grid item),
|
||||
// not from the library panel definition.
|
||||
// Exception: Public dashboards and scripted dashboards still need this migration
|
||||
// even when dashboardNewLayouts is enabled. This is because public and scripted dashboard migrations are still handled in the frontend.
|
||||
const dashboard = getDashboardSceneFor(this);
|
||||
const isPublicDashboard = dashboard.state.meta.publicDashboardEnabled === true;
|
||||
const isScriptedDashboard = dashboard.state.meta.fromScript === true;
|
||||
const shouldSkipRepeatMigration =
|
||||
config.featureToggles.dashboardNewLayouts && !isPublicDashboard && !isScriptedDashboard;
|
||||
|
||||
// Migrate repeat options to layout element (only for legacy dashboards, or public/scripted dashboards)
|
||||
if (!shouldSkipRepeatMigration && libPanelModel.repeat && layoutElement instanceof DashboardGridItem) {
|
||||
layoutElement.setState({
|
||||
variableName: libPanelModel.repeat,
|
||||
repeatDirection: libPanelModel.repeatDirection === 'h' ? 'h' : 'v',
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
Spec as DashboardV2Spec,
|
||||
GridLayoutItemKind,
|
||||
RowsLayoutRowKind,
|
||||
} from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
|
||||
/**
|
||||
* Normalizes backend output to match frontend behavior.
|
||||
* The backend sets repeat properties on library panel grid items from the library panel definition,
|
||||
* but the frontend only sets repeat when explicitly set on the panel instance.
|
||||
* This function removes repeat properties from library panel items where they weren't set on the instance.
|
||||
*
|
||||
* The difference in behavior is due to how the frontend conversion is done.
|
||||
* It is not feasible to fetch all library panels async in all cases where the transformation is done.
|
||||
* Library panel repeats will be set by the library panel behavior in those cases.
|
||||
*
|
||||
* This is a temporary solution until public dashboards and scripted dashboards have proper backend conversions.
|
||||
*/
|
||||
export function normalizeBackendOutputForFrontendComparison(
|
||||
backendSpec: DashboardV2Spec,
|
||||
inputPanels: Array<{
|
||||
id?: number;
|
||||
libraryPanel?: { uid?: string };
|
||||
repeat?: string;
|
||||
gridPos?: { w?: number; h?: number; x?: number; y?: number };
|
||||
}>
|
||||
): DashboardV2Spec {
|
||||
// Deep clone the spec to avoid mutating the original
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const normalized = JSON.parse(JSON.stringify(backendSpec)) as DashboardV2Spec;
|
||||
|
||||
// Create a map of panel ID to whether it has explicit repeat, original width, and if it's a library panel
|
||||
const panelHasExplicitRepeat = new Map<number, boolean>();
|
||||
const panelOriginalWidth = new Map<number, number>();
|
||||
const panelIsLibraryPanel = new Map<number, boolean>();
|
||||
inputPanels.forEach((panel) => {
|
||||
if (panel.id !== undefined) {
|
||||
panelHasExplicitRepeat.set(panel.id, !!panel.repeat);
|
||||
panelIsLibraryPanel.set(panel.id, !!panel.libraryPanel);
|
||||
if (panel.gridPos?.w !== undefined) {
|
||||
panelOriginalWidth.set(panel.id, panel.gridPos.w);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to recursively process grid items
|
||||
function processGridItems(items: GridLayoutItemKind[]): void {
|
||||
if (!Array.isArray(items)) {
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach((item) => {
|
||||
if (item.spec?.element?.name) {
|
||||
// Extract panel ID from element name (format: "panel-{id}")
|
||||
const match = item.spec.element.name.match(/^panel-(\d+)$/);
|
||||
if (match) {
|
||||
const panelId = parseInt(match[1], 10);
|
||||
const hasExplicitRepeat = panelHasExplicitRepeat.get(panelId);
|
||||
const isLibraryPanel = panelIsLibraryPanel.get(panelId);
|
||||
|
||||
// Only normalize library panels - check both input panel and element kind
|
||||
const element = normalized.elements?.[item.spec.element.name];
|
||||
const isElementLibraryPanel = element?.kind === 'LibraryPanel';
|
||||
|
||||
// If this is a library panel item and repeat wasn't explicitly set on the instance,
|
||||
// remove the repeat property (backend adds it from library panel definition)
|
||||
// Also restore the original width when removing repeat properties
|
||||
if ((isLibraryPanel || isElementLibraryPanel) && hasExplicitRepeat === false && item.spec.repeat) {
|
||||
delete item.spec.repeat;
|
||||
// Always restore the original width from the input panel
|
||||
const originalWidth = panelOriginalWidth.get(panelId);
|
||||
if (originalWidth !== undefined) {
|
||||
item.spec.width = originalWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Process GridLayout items
|
||||
if (normalized.layout?.kind === 'GridLayout' && normalized.layout.spec?.items) {
|
||||
processGridItems(normalized.layout.spec.items);
|
||||
}
|
||||
|
||||
// Process RowsLayout items
|
||||
if (normalized.layout?.kind === 'RowsLayout' && normalized.layout.spec?.rows) {
|
||||
normalized.layout.spec.rows.forEach((row: RowsLayoutRowKind) => {
|
||||
if (row.spec?.layout?.kind === 'GridLayout' && row.spec.layout.spec?.items) {
|
||||
processGridItems(row.spec.layout.spec.items);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { readdirSync, readFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { normalizeBackendOutputForFrontendComparison } from './serialization-test-utils';
|
||||
import { transformSaveModelSchemaV2ToScene } from './transformSaveModelSchemaV2ToScene';
|
||||
import { transformSaveModelToScene } from './transformSaveModelToScene';
|
||||
import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSchemaV2';
|
||||
@@ -229,8 +230,17 @@ describe('V1 to V2 Dashboard Transformation Comparison', () => {
|
||||
expect(frontendOutput).toBeDefined();
|
||||
expect(backendOutputAfterLoadedByScene).toBeDefined();
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
// Compare only the spec structures - this is the core transformation
|
||||
expect(backendOutputAfterLoadedByScene).toEqual(frontendOutput);
|
||||
expect(normalizedBackendOutput).toEqual(frontendOutput);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -292,8 +302,17 @@ describe('V1 to V2 Dashboard Transformation Comparison', () => {
|
||||
expect(frontendOutput).toBeDefined();
|
||||
expect(backendOutputAfterLoadedByScene).toBeDefined();
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
// Compare only the spec structures - this is the core transformation
|
||||
expect(backendOutputAfterLoadedByScene).toEqual(frontendOutput);
|
||||
expect(normalizedBackendOutput).toEqual(frontendOutput);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from 'path';
|
||||
|
||||
import { mockDataSource } from 'app/features/alerting/unified/mocks';
|
||||
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
|
||||
import { normalizeBackendOutputForFrontendComparison } from 'app/features/dashboard-scene/serialization/serialization-test-utils';
|
||||
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||
|
||||
import { ensureV2Response } from './ResponseTransformers';
|
||||
@@ -205,8 +206,13 @@ describe('Backend / Frontend result comparison', () => {
|
||||
expect(frontendOutput.spec).toBeDefined();
|
||||
expect(backendOutput.spec).toBeDefined();
|
||||
|
||||
// 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
|
||||
const inputPanels = jsonInput.spec?.panels || [];
|
||||
const normalizedBackendSpec = normalizeBackendOutputForFrontendComparison(backendOutput.spec, inputPanels);
|
||||
|
||||
// Compare the spec structures
|
||||
expect(backendOutput.spec).toEqual(frontendOutput.spec);
|
||||
expect(normalizedBackendSpec).toEqual(frontendOutput.spec);
|
||||
|
||||
// Verify the conversion doesn't throw errors and produces a valid structure
|
||||
expect(() => JSON.stringify(frontendOutput)).not.toThrow();
|
||||
|
||||
Reference in New Issue
Block a user