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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user