Dashboards: Remove panel plugin provider from migrations (#110477)
This commit is contained in:
@@ -31,7 +31,7 @@ import (
|
||||
|
||||
func TestConversionMatrixExist(t *testing.T) {
|
||||
// Initialize the migrator with a test data source provider
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider(), migrationtestutil.GetTestPanelProvider())
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider())
|
||||
|
||||
versions := []metav1.Object{
|
||||
&dashv0.Dashboard{Spec: common.Unstructured{Object: map[string]any{"title": "dashboardV0"}}},
|
||||
@@ -82,7 +82,7 @@ func TestDeepCopyValid(t *testing.T) {
|
||||
|
||||
func TestDashboardConversionToAllVersions(t *testing.T) {
|
||||
// Initialize the migrator with a test data source provider
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider(), migrationtestutil.GetTestPanelProvider())
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider())
|
||||
|
||||
// Set up conversion scheme
|
||||
scheme := runtime.NewScheme()
|
||||
@@ -234,7 +234,7 @@ func testConversion(t *testing.T, convertedDash metav1.Object, filename, outputD
|
||||
// TestConversionMetrics tests that conversion-level metrics are recorded correctly
|
||||
func TestConversionMetrics(t *testing.T) {
|
||||
// Initialize migration with test providers
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider(), migrationtestutil.GetTestPanelProvider())
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider())
|
||||
|
||||
// Create a test registry for metrics
|
||||
registry := prometheus.NewRegistry()
|
||||
@@ -365,7 +365,7 @@ func TestConversionMetrics(t *testing.T) {
|
||||
|
||||
// TestConversionMetricsWrapper tests the withConversionMetrics wrapper function
|
||||
func TestConversionMetricsWrapper(t *testing.T) {
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider(), migrationtestutil.GetTestPanelProvider())
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider())
|
||||
|
||||
// Create a test registry for metrics
|
||||
registry := prometheus.NewRegistry()
|
||||
@@ -521,7 +521,7 @@ func TestSchemaVersionExtraction(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Test the schema version extraction logic by creating a wrapper and checking the metrics labels
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider(), migrationtestutil.GetTestPanelProvider())
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider())
|
||||
|
||||
// Create a test registry for metrics
|
||||
registry := prometheus.NewRegistry()
|
||||
@@ -564,7 +564,7 @@ func TestSchemaVersionExtraction(t *testing.T) {
|
||||
|
||||
// TestConversionLogging tests that conversion-level logging works correctly
|
||||
func TestConversionLogging(t *testing.T) {
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider(), migrationtestutil.GetTestPanelProvider())
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider())
|
||||
|
||||
// Create a test registry for metrics
|
||||
registry := prometheus.NewRegistry()
|
||||
@@ -654,7 +654,7 @@ func TestConversionLogging(t *testing.T) {
|
||||
|
||||
// TestConversionLogLevels tests that appropriate log levels are used
|
||||
func TestConversionLogLevels(t *testing.T) {
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider(), migrationtestutil.GetTestPanelProvider())
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider())
|
||||
|
||||
t.Run("log levels and structured fields verification", func(t *testing.T) {
|
||||
// Create test wrapper to verify logging behavior
|
||||
@@ -723,7 +723,7 @@ func TestConversionLogLevels(t *testing.T) {
|
||||
|
||||
// TestConversionLoggingFields tests that all expected fields are included in log messages
|
||||
func TestConversionLoggingFields(t *testing.T) {
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider(), migrationtestutil.GetTestPanelProvider())
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider())
|
||||
|
||||
t.Run("verify all log fields are present", func(t *testing.T) {
|
||||
// Test that the conversion wrapper includes all expected structured fields
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
)
|
||||
|
||||
// Initialize provides the migrator singleton with required dependencies and builds the map of migrations.
|
||||
func Initialize(dsInfoProvider schemaversion.DataSourceInfoProvider, panelProvider schemaversion.PanelPluginInfoProvider) {
|
||||
migratorInstance.init(dsInfoProvider, panelProvider)
|
||||
func Initialize(dsInfoProvider schemaversion.DataSourceInfoProvider) {
|
||||
migratorInstance.init(dsInfoProvider)
|
||||
}
|
||||
|
||||
// Migrate migrates the given dashboard to the target version.
|
||||
@@ -32,9 +32,9 @@ type migrator struct {
|
||||
migrations map[int]schemaversion.SchemaVersionMigrationFunc
|
||||
}
|
||||
|
||||
func (m *migrator) init(dsInfoProvider schemaversion.DataSourceInfoProvider, panelProvider schemaversion.PanelPluginInfoProvider) {
|
||||
func (m *migrator) init(dsInfoProvider schemaversion.DataSourceInfoProvider) {
|
||||
initOnce.Do(func() {
|
||||
m.migrations = schemaversion.GetMigrations(dsInfoProvider, panelProvider)
|
||||
m.migrations = schemaversion.GetMigrations(dsInfoProvider)
|
||||
close(m.ready)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestMigrate(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use the same datasource provider as the frontend test to ensure consistency
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider(), migrationtestutil.GetTestPanelProvider())
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider())
|
||||
|
||||
t.Run("minimum version check", func(t *testing.T) {
|
||||
err := migration.Migrate(context.Background(), map[string]interface{}{
|
||||
@@ -122,7 +122,7 @@ func loadDashboard(t *testing.T, path string) map[string]interface{} {
|
||||
// TestSchemaMigrationMetrics tests that schema migration metrics are recorded correctly
|
||||
func TestSchemaMigrationMetrics(t *testing.T) {
|
||||
// Initialize migration with test providers
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider(), migrationtestutil.GetTestPanelProvider())
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider())
|
||||
|
||||
// Create a test registry for metrics
|
||||
registry := prometheus.NewRegistry()
|
||||
@@ -206,7 +206,7 @@ func TestSchemaMigrationMetrics(t *testing.T) {
|
||||
|
||||
// TestSchemaMigrationLogging tests that schema migration logging works correctly
|
||||
func TestSchemaMigrationLogging(t *testing.T) {
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider(), migrationtestutil.GetTestPanelProvider())
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -286,7 +286,7 @@ func TestSchemaMigrationLogging(t *testing.T) {
|
||||
|
||||
// TestLogMessageStructure tests that log messages contain expected structured fields
|
||||
func TestLogMessageStructure(t *testing.T) {
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider(), migrationtestutil.GetTestPanelProvider())
|
||||
migration.Initialize(migrationtestutil.GetTestDataSourceProvider())
|
||||
|
||||
t.Run("log messages include all required fields", func(t *testing.T) {
|
||||
// Test that migration functions execute successfully, ensuring log code paths are hit
|
||||
|
||||
@@ -9,6 +9,9 @@ import (
|
||||
const (
|
||||
MIN_VERSION = 13
|
||||
LATEST_VERSION = 41
|
||||
|
||||
// The pluginVersion to set after simulating auto-migrate for angular panels
|
||||
pluginVersionForAutoMigrate = "12.1.0"
|
||||
)
|
||||
|
||||
type SchemaVersionMigrationFunc func(context.Context, map[string]interface{}) error
|
||||
@@ -33,14 +36,7 @@ type PanelPluginInfo struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
type PanelPluginInfoProvider interface {
|
||||
// Gets all the panels from the plugin store.
|
||||
// Equivalent to grafanaBootData.settings.panels on the frontend.
|
||||
GetPanels() []PanelPluginInfo
|
||||
GetPanelPlugin(id string) PanelPluginInfo
|
||||
}
|
||||
|
||||
func GetMigrations(dsInfoProvider DataSourceInfoProvider, panelProvider PanelPluginInfoProvider) map[int]SchemaVersionMigrationFunc {
|
||||
func GetMigrations(dsInfoProvider DataSourceInfoProvider) map[int]SchemaVersionMigrationFunc {
|
||||
return map[int]SchemaVersionMigrationFunc{
|
||||
14: V14,
|
||||
15: V15,
|
||||
@@ -52,11 +48,11 @@ func GetMigrations(dsInfoProvider DataSourceInfoProvider, panelProvider PanelPlu
|
||||
21: V21,
|
||||
22: V22,
|
||||
23: V23,
|
||||
24: V24(panelProvider),
|
||||
24: V24,
|
||||
25: V25,
|
||||
26: V26,
|
||||
27: V27,
|
||||
28: V28(panelProvider),
|
||||
28: V28,
|
||||
29: V29,
|
||||
30: V30,
|
||||
31: V31,
|
||||
|
||||
@@ -4,8 +4,9 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
)
|
||||
|
||||
func TestGetSchemaVersion(t *testing.T) {
|
||||
|
||||
@@ -65,7 +65,7 @@ import (
|
||||
// },
|
||||
// "transformations": [],
|
||||
// "targets": [{ "refId": "A" }],
|
||||
// "pluginVersion": "1.0.0"
|
||||
// "pluginVersion": "{current_grafana_version}"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
@@ -181,26 +181,12 @@ import (
|
||||
// }
|
||||
// ],
|
||||
// "targets": [{ "refId": "A" }],
|
||||
// "pluginVersion": "1.0.0"
|
||||
// "pluginVersion": "{current_grafana_version}"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
type v24Migrator struct {
|
||||
panelProvider PanelPluginInfoProvider
|
||||
panelPlugins []PanelPluginInfo
|
||||
}
|
||||
|
||||
func V24(panelProvider PanelPluginInfoProvider) SchemaVersionMigrationFunc {
|
||||
migrator := &v24Migrator{
|
||||
panelProvider: panelProvider,
|
||||
panelPlugins: panelProvider.GetPanels(),
|
||||
}
|
||||
|
||||
return migrator.migrate
|
||||
}
|
||||
|
||||
func (m *v24Migrator) migrate(_ context.Context, dashboard map[string]interface{}) error {
|
||||
func V24(_ context.Context, dashboard map[string]interface{}) error {
|
||||
dashboard["schemaVersion"] = 24
|
||||
|
||||
panels, ok := dashboard["panels"].([]interface{})
|
||||
@@ -225,12 +211,8 @@ func (m *v24Migrator) migrate(_ context.Context, dashboard map[string]interface{
|
||||
continue
|
||||
}
|
||||
|
||||
// Find if the panel plugin exists
|
||||
tablePanelPlugin := m.panelProvider.GetPanelPlugin("table")
|
||||
if tablePanelPlugin.ID == "" {
|
||||
return NewMigrationError("table panel plugin not found when migrating dashboard to schema version 24", 24, LATEST_VERSION, "V24")
|
||||
}
|
||||
panelMap["pluginVersion"] = tablePanelPlugin.Version
|
||||
// The grafana version that matches the hardcoded autoMigrate plugins
|
||||
panelMap["pluginVersion"] = pluginVersionForAutoMigrate
|
||||
err := tablePanelChangedHandler(panelMap)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,7 +4,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil"
|
||||
)
|
||||
|
||||
const (
|
||||
// The pluginVersion to set after simulating auto-migrate for angular panels
|
||||
pluginVersionForAutoMigrate = "12.1.0"
|
||||
)
|
||||
|
||||
func TestV24(t *testing.T) {
|
||||
@@ -79,7 +83,7 @@ func TestV24(t *testing.T) {
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": pluginVersionForAutoMigrate,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -241,7 +245,7 @@ func TestV24(t *testing.T) {
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": pluginVersionForAutoMigrate,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -331,7 +335,7 @@ func TestV24(t *testing.T) {
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": pluginVersionForAutoMigrate,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -410,7 +414,7 @@ func TestV24(t *testing.T) {
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": pluginVersionForAutoMigrate,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -489,7 +493,7 @@ func TestV24(t *testing.T) {
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": pluginVersionForAutoMigrate,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -567,7 +571,7 @@ func TestV24(t *testing.T) {
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": pluginVersionForAutoMigrate,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -664,7 +668,7 @@ func TestV24(t *testing.T) {
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": pluginVersionForAutoMigrate,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -836,12 +840,12 @@ func TestV24(t *testing.T) {
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": pluginVersionForAutoMigrate,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runMigrationTests(t, tests, schemaversion.V24(testutil.GetTestPanelProvider()))
|
||||
runMigrationTests(t, tests, schemaversion.V24)
|
||||
}
|
||||
|
||||
@@ -47,37 +47,12 @@ import (
|
||||
// { "name": "var1" }
|
||||
// ]
|
||||
// }
|
||||
type v28Migrator struct {
|
||||
panelProvider PanelPluginInfoProvider
|
||||
panelPlugins []PanelPluginInfo
|
||||
statPanelVersion string // Cached stat panel version
|
||||
}
|
||||
|
||||
func V28(panelProvider PanelPluginInfoProvider) SchemaVersionMigrationFunc {
|
||||
// Get stat panel version once during initialization
|
||||
statPanelPlugin := panelProvider.GetPanelPlugin("stat")
|
||||
statPanelVersion := ""
|
||||
if statPanelPlugin.ID != "" {
|
||||
statPanelVersion = statPanelPlugin.Version
|
||||
}
|
||||
|
||||
migrator := &v28Migrator{
|
||||
panelProvider: panelProvider,
|
||||
panelPlugins: panelProvider.GetPanels(),
|
||||
statPanelVersion: statPanelVersion,
|
||||
}
|
||||
|
||||
return func(ctx context.Context, dashboard map[string]interface{}) error {
|
||||
return migrator.migrate(context.Background(), dashboard)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *v28Migrator) migrate(_ context.Context, dashboard map[string]interface{}) error {
|
||||
func V28(_ context.Context, dashboard map[string]interface{}) error {
|
||||
dashboard["schemaVersion"] = 28
|
||||
|
||||
// Migrate singlestat panels
|
||||
if panels, ok := dashboard["panels"].([]interface{}); ok {
|
||||
if err := m.processPanels(panels); err != nil {
|
||||
if err := processPanels(panels); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -96,7 +71,7 @@ func (m *v28Migrator) migrate(_ context.Context, dashboard map[string]interface{
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *v28Migrator) processPanels(panels []interface{}) error {
|
||||
func processPanels(panels []interface{}) error {
|
||||
for _, panel := range panels {
|
||||
p, ok := panel.(map[string]interface{})
|
||||
if !ok {
|
||||
@@ -106,7 +81,7 @@ func (m *v28Migrator) processPanels(panels []interface{}) error {
|
||||
// Process nested panels if this is a row panel
|
||||
if p["type"] == "row" {
|
||||
if nestedPanels, ok := p["panels"].([]interface{}); ok {
|
||||
if err := m.processPanels(nestedPanels); err != nil {
|
||||
if err := processPanels(nestedPanels); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -115,23 +90,27 @@ func (m *v28Migrator) processPanels(panels []interface{}) error {
|
||||
|
||||
// Migrate singlestat panels
|
||||
if p["type"] == "singlestat" || p["type"] == "grafana-singlestat-panel" {
|
||||
if err := m.migrateSinglestatPanel(p); err != nil {
|
||||
if err := migrateSinglestatPanel(p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize existing stat panels to ensure they have current default options
|
||||
if p["type"] == "stat" {
|
||||
m.normalizeStatPanel(p)
|
||||
normalizeStatPanel(p)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *v28Migrator) migrateSinglestatPanel(panel map[string]interface{}) error {
|
||||
func migrateSinglestatPanel(panel map[string]interface{}) error {
|
||||
targetType := "stat"
|
||||
|
||||
// NOTE: The legacy types "singlestat" and "gauge" are both angular only
|
||||
// This are not supported by any version that could run this migration, so there is
|
||||
// no need to maintain a distinction or fallback to the non-stat version
|
||||
|
||||
// NOTE: DashboardMigrator's migrateSinglestat function has some logic that never gets called
|
||||
// migrateSinglestat will only run if (panel.type === 'singlestat')
|
||||
// but this will not be the case because PanelModel runs restoreModel in the constructor
|
||||
@@ -146,22 +125,16 @@ func (m *v28Migrator) migrateSinglestatPanel(panel map[string]interface{}) error
|
||||
originalType := panel["type"].(string)
|
||||
panel["autoMigrateFrom"] = panel["type"]
|
||||
panel["type"] = targetType
|
||||
|
||||
// Use cached stat panel version
|
||||
if m.statPanelVersion == "" {
|
||||
return NewMigrationError("stat panel plugin not found when migrating dashboard to schema version 28", 28, LATEST_VERSION, "V28")
|
||||
}
|
||||
|
||||
panel["pluginVersion"] = m.statPanelVersion
|
||||
panel["pluginVersion"] = pluginVersionForAutoMigrate
|
||||
|
||||
// Migrate panel options and field config
|
||||
m.migrateSinglestatOptions(panel, originalType)
|
||||
migrateSinglestatOptions(panel, originalType)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeStatPanel ensures existing stat panels have all current default options
|
||||
func (m *v28Migrator) normalizeStatPanel(panel map[string]interface{}) {
|
||||
func normalizeStatPanel(panel map[string]interface{}) {
|
||||
if panel["options"] == nil {
|
||||
panel["options"] = map[string]interface{}{}
|
||||
}
|
||||
@@ -192,7 +165,7 @@ func (m *v28Migrator) normalizeStatPanel(panel map[string]interface{}) {
|
||||
}
|
||||
|
||||
// migrateSinglestatOptions handles the complete migration of singlestat panel options and field config
|
||||
func (m *v28Migrator) migrateSinglestatOptions(panel map[string]interface{}, originalType string) {
|
||||
func migrateSinglestatOptions(panel map[string]interface{}, originalType string) {
|
||||
// Initialize field config if not present
|
||||
if panel["fieldConfig"] == nil {
|
||||
panel["fieldConfig"] = map[string]interface{}{
|
||||
@@ -206,20 +179,20 @@ func (m *v28Migrator) migrateSinglestatOptions(panel map[string]interface{}, ori
|
||||
|
||||
// Migrate from angular singlestat configuration using appropriate strategy
|
||||
if originalType == "grafana-singlestat-panel" {
|
||||
m.migrateGrafanaSinglestatPanel(panel, defaults)
|
||||
migrateGrafanaSinglestatPanel(panel, defaults)
|
||||
} else {
|
||||
m.migratetSinglestat(panel, defaults)
|
||||
migratetSinglestat(panel, defaults)
|
||||
}
|
||||
|
||||
// Apply shared migration logic
|
||||
m.applySharedSinglestatMigration(defaults)
|
||||
applySharedSinglestatMigration(defaults)
|
||||
|
||||
// Clean up old angular properties after migration
|
||||
m.cleanupAngularProperties(panel)
|
||||
cleanupAngularProperties(panel)
|
||||
}
|
||||
|
||||
// getDefaultStatOptions returns the default options structure for stat panels
|
||||
func (m *v28Migrator) getDefaultStatOptions() map[string]interface{} {
|
||||
func getDefaultStatOptions() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"reduceOptions": map[string]interface{}{
|
||||
"calcs": []string{"mean"},
|
||||
@@ -237,11 +210,11 @@ func (m *v28Migrator) getDefaultStatOptions() map[string]interface{} {
|
||||
|
||||
// migratetSinglestat handles explicit migration from 'singlestat' panels
|
||||
// Based on explicit migration logic in DashboardMigrator.ts
|
||||
func (m *v28Migrator) migratetSinglestat(panel map[string]interface{}, defaults map[string]interface{}) {
|
||||
angularOpts := m.extractAngularOptions(panel)
|
||||
func migratetSinglestat(panel map[string]interface{}, defaults map[string]interface{}) {
|
||||
angularOpts := extractAngularOptions(panel)
|
||||
|
||||
// Explicit migration uses standard stat panel defaults
|
||||
options := m.getDefaultStatOptions()
|
||||
options := getDefaultStatOptions()
|
||||
|
||||
// Explicit migration: always set a reducer with fallback
|
||||
var valueName string
|
||||
@@ -249,7 +222,7 @@ func (m *v28Migrator) migratetSinglestat(panel map[string]interface{}, defaults
|
||||
valueName = vn
|
||||
}
|
||||
|
||||
if reducer := m.getReducerForValueName(valueName); reducer != "" {
|
||||
if reducer := getReducerForValueName(valueName); reducer != "" {
|
||||
options["reduceOptions"].(map[string]interface{})["calcs"] = []string{reducer}
|
||||
} else {
|
||||
// Explicit migration fallback: use mean for invalid reducers
|
||||
@@ -257,7 +230,7 @@ func (m *v28Migrator) migratetSinglestat(panel map[string]interface{}, defaults
|
||||
}
|
||||
|
||||
// Migrate thresholds FIRST (consolidated: both panel types create DEFAULT_THRESHOLDS for empty strings)
|
||||
m.migrateThresholds(angularOpts, defaults)
|
||||
migrateThresholds(angularOpts, defaults)
|
||||
|
||||
// If no thresholds were set from angular migration, add default stat panel thresholds
|
||||
// This matches the behavior of frontend pluginLoaded which adds default thresholds
|
||||
@@ -278,15 +251,15 @@ func (m *v28Migrator) migratetSinglestat(panel map[string]interface{}, defaults
|
||||
}
|
||||
|
||||
// Apply common angular option migrations (value mappings can now use threshold colors)
|
||||
m.applyCommonAngularMigration(panel, defaults, options, angularOpts)
|
||||
applyCommonAngularMigration(panel, defaults, options, angularOpts)
|
||||
|
||||
panel["options"] = options
|
||||
}
|
||||
|
||||
// migrateGrafanaSinglestatPanel handles auto-migration from 'grafana-singlestat-panel'
|
||||
// Based on frontend changePlugin() and sharedSingleStatPanelChangedHandler logic
|
||||
func (m *v28Migrator) migrateGrafanaSinglestatPanel(panel map[string]interface{}, defaults map[string]interface{}) {
|
||||
angularOpts := m.extractAngularOptions(panel)
|
||||
func migrateGrafanaSinglestatPanel(panel map[string]interface{}, defaults map[string]interface{}) {
|
||||
angularOpts := extractAngularOptions(panel)
|
||||
|
||||
// Auto-migration uses different defaults (matches frontend changePlugin behavior)
|
||||
options := map[string]interface{}{
|
||||
@@ -309,13 +282,13 @@ func (m *v28Migrator) migrateGrafanaSinglestatPanel(panel map[string]interface{}
|
||||
valueName = vn
|
||||
}
|
||||
|
||||
if reducer := m.getReducerForValueName(valueName); reducer != "" {
|
||||
if reducer := getReducerForValueName(valueName); reducer != "" {
|
||||
options["reduceOptions"].(map[string]interface{})["calcs"] = []string{reducer}
|
||||
}
|
||||
// No fallback - keeps the auto-migration default "lastNotNull"
|
||||
|
||||
// Migrate thresholds FIRST (consolidated: both panel types create DEFAULT_THRESHOLDS for empty strings)
|
||||
m.migrateThresholds(angularOpts, defaults)
|
||||
migrateThresholds(angularOpts, defaults)
|
||||
|
||||
// If no thresholds were set from angular migration, add default stat panel thresholds
|
||||
// This matches the behavior of frontend pluginLoaded which adds default thresholds
|
||||
@@ -336,19 +309,19 @@ func (m *v28Migrator) migrateGrafanaSinglestatPanel(panel map[string]interface{}
|
||||
}
|
||||
|
||||
// Apply common angular option migrations (value mappings can now use threshold colors)
|
||||
m.applyCommonAngularMigration(panel, defaults, options, angularOpts)
|
||||
applyCommonAngularMigration(panel, defaults, options, angularOpts)
|
||||
|
||||
panel["options"] = options
|
||||
}
|
||||
|
||||
// migrateThresholds handles threshold migration for both singlestat panel types
|
||||
// Both panel types now create DEFAULT_THRESHOLDS when threshold string is empty (consolidated behavior)
|
||||
func (m *v28Migrator) migrateThresholds(angularOpts map[string]interface{}, defaults map[string]interface{}) {
|
||||
func migrateThresholds(angularOpts map[string]interface{}, defaults map[string]interface{}) {
|
||||
if thresholds, ok := angularOpts["thresholds"].(string); ok {
|
||||
if colors, ok := angularOpts["colors"].([]interface{}); ok {
|
||||
if thresholds != "" {
|
||||
// Non-empty thresholds: use normal migration
|
||||
m.migrateThresholdsAndColors(defaults, thresholds, colors)
|
||||
migrateThresholdsAndColors(defaults, thresholds, colors)
|
||||
} else {
|
||||
// Empty thresholds: use frontend DEFAULT_THRESHOLDS fallback (both panel types)
|
||||
defaults["thresholds"] = map[string]interface{}{
|
||||
@@ -370,7 +343,7 @@ func (m *v28Migrator) migrateThresholds(angularOpts map[string]interface{}, defa
|
||||
}
|
||||
|
||||
// applyCommonAngularMigration applies migrations common to both singlestat types
|
||||
func (m *v28Migrator) applyCommonAngularMigration(panel map[string]interface{}, defaults map[string]interface{}, options map[string]interface{}, angularOpts map[string]interface{}) {
|
||||
func applyCommonAngularMigration(panel map[string]interface{}, defaults map[string]interface{}, options map[string]interface{}, angularOpts map[string]interface{}) {
|
||||
// Migrate table column
|
||||
// Based on sharedSingleStatPanelChangedHandler line ~125: options.reduceOptions.fields = `/^${prevPanel.tableColumn}$/`
|
||||
if tableColumn, ok := angularOpts["tableColumn"].(string); ok && tableColumn != "" {
|
||||
@@ -400,7 +373,7 @@ func (m *v28Migrator) applyCommonAngularMigration(panel map[string]interface{},
|
||||
|
||||
// Migrate value mappings (thresholds should already be migrated)
|
||||
valueMaps, _ := angularOpts["valueMaps"].([]interface{})
|
||||
m.migrateValueMappings(angularOpts, defaults, valueMaps)
|
||||
migrateValueMappings(angularOpts, defaults, valueMaps)
|
||||
|
||||
// Migrate sparkline configuration
|
||||
// Based on statPanelChangedHandler lines ~25-35: sparkline migration logic
|
||||
@@ -448,7 +421,7 @@ func (m *v28Migrator) applyCommonAngularMigration(panel map[string]interface{},
|
||||
|
||||
// applySharedSinglestatMigration applies shared migration logic for all singlestat panels
|
||||
// Based on sharedSingleStatMigrationHandler in packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts
|
||||
func (m *v28Migrator) applySharedSinglestatMigration(defaults map[string]interface{}) {
|
||||
func applySharedSinglestatMigration(defaults map[string]interface{}) {
|
||||
// Ensure thresholds have proper structure
|
||||
if thresholds, ok := defaults["thresholds"].(map[string]interface{}); ok {
|
||||
if steps, ok := thresholds["steps"].([]interface{}); ok {
|
||||
@@ -487,7 +460,7 @@ func (m *v28Migrator) applySharedSinglestatMigration(defaults map[string]interfa
|
||||
|
||||
// Helper functions
|
||||
|
||||
func (m *v28Migrator) extractAngularOptions(panel map[string]interface{}) map[string]interface{} {
|
||||
func extractAngularOptions(panel map[string]interface{}) map[string]interface{} {
|
||||
// Some panels might have angular options directly in the root
|
||||
// Check for common angular properties
|
||||
angularProps := []string{
|
||||
@@ -504,7 +477,7 @@ func (m *v28Migrator) extractAngularOptions(panel map[string]interface{}) map[st
|
||||
}
|
||||
|
||||
// getReducerForValueName returns the mapped reducer or empty string for invalid values
|
||||
func (m *v28Migrator) getReducerForValueName(valueName string) string {
|
||||
func getReducerForValueName(valueName string) string {
|
||||
reducerMap := map[string]string{
|
||||
"min": "min",
|
||||
"max": "max",
|
||||
@@ -526,7 +499,7 @@ func (m *v28Migrator) getReducerForValueName(valueName string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *v28Migrator) migrateThresholdsAndColors(defaults map[string]interface{}, thresholdsStr string, colors []interface{}) {
|
||||
func migrateThresholdsAndColors(defaults map[string]interface{}, thresholdsStr string, colors []interface{}) {
|
||||
// Parse thresholds string (e.g., "10,20,30")
|
||||
// Based on sharedSingleStatPanelChangedHandler lines ~145-165: Convert thresholds and color values
|
||||
thresholds := []interface{}{}
|
||||
@@ -555,7 +528,7 @@ func (m *v28Migrator) migrateThresholdsAndColors(defaults map[string]interface{}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *v28Migrator) migrateValueMappings(panel map[string]interface{}, defaults map[string]interface{}, valueMappings []interface{}) {
|
||||
func migrateValueMappings(panel map[string]interface{}, defaults map[string]interface{}, valueMappings []interface{}) {
|
||||
mappings := []interface{}{}
|
||||
mappingType := panel["mappingType"]
|
||||
|
||||
@@ -571,7 +544,7 @@ func (m *v28Migrator) migrateValueMappings(panel map[string]interface{}, default
|
||||
case 1:
|
||||
for _, valueMap := range valueMappings {
|
||||
valueMapping := valueMap.(map[string]interface{})
|
||||
upgradedMapping := m.upgradeOldAngularValueMapping(valueMapping, defaults["thresholds"])
|
||||
upgradedMapping := upgradeOldAngularValueMapping(valueMapping, defaults["thresholds"])
|
||||
if upgradedMapping != nil {
|
||||
mappings = append(mappings, upgradedMapping)
|
||||
}
|
||||
@@ -581,7 +554,7 @@ func (m *v28Migrator) migrateValueMappings(panel map[string]interface{}, default
|
||||
if rangeMaps, ok := panel["rangeMaps"].([]interface{}); ok {
|
||||
for _, rangeMap := range rangeMaps {
|
||||
rangeMapping := rangeMap.(map[string]interface{})
|
||||
upgradedMapping := m.upgradeOldAngularValueMapping(rangeMapping, defaults["thresholds"])
|
||||
upgradedMapping := upgradeOldAngularValueMapping(rangeMapping, defaults["thresholds"])
|
||||
if upgradedMapping != nil {
|
||||
mappings = append(mappings, upgradedMapping)
|
||||
}
|
||||
@@ -594,7 +567,7 @@ func (m *v28Migrator) migrateValueMappings(panel map[string]interface{}, default
|
||||
|
||||
// upgradeOldAngularValueMapping converts old angular value mappings to new format
|
||||
// Based on upgradeOldAngularValueMapping in packages/grafana-data/src/utils/valueMappings.ts
|
||||
func (m *v28Migrator) upgradeOldAngularValueMapping(old map[string]interface{}, thresholds interface{}) map[string]interface{} {
|
||||
func upgradeOldAngularValueMapping(old map[string]interface{}, thresholds interface{}) map[string]interface{} {
|
||||
valueMaps := map[string]interface{}{
|
||||
"type": "value",
|
||||
"options": map[string]interface{}{},
|
||||
@@ -604,10 +577,10 @@ func (m *v28Migrator) upgradeOldAngularValueMapping(old map[string]interface{},
|
||||
// Use the color we would have picked from thresholds
|
||||
var color interface{}
|
||||
if value, ok := old["value"]; ok {
|
||||
if numeric, err := m.parseNumericValue(value); err == nil {
|
||||
if numeric, err := parseNumericValue(value); err == nil {
|
||||
if thresholdsMap, ok := thresholds.(map[string]interface{}); ok {
|
||||
if steps, ok := thresholdsMap["steps"].([]interface{}); ok {
|
||||
level := m.getActiveThreshold(numeric, steps)
|
||||
level := getActiveThreshold(numeric, steps)
|
||||
if level != nil {
|
||||
if levelColor, ok := level["color"]; ok {
|
||||
color = levelColor
|
||||
@@ -704,7 +677,7 @@ func (m *v28Migrator) upgradeOldAngularValueMapping(old map[string]interface{},
|
||||
|
||||
// getActiveThreshold finds the active threshold for a given value
|
||||
// Based on getActiveThreshold in packages/grafana-data/src/field/thresholds.ts
|
||||
func (m *v28Migrator) getActiveThreshold(value float64, steps []interface{}) map[string]interface{} {
|
||||
func getActiveThreshold(value float64, steps []interface{}) map[string]interface{} {
|
||||
for i := len(steps) - 1; i >= 0; i-- {
|
||||
if step, ok := steps[i].(map[string]interface{}); ok {
|
||||
if stepValue, ok := step["value"]; ok {
|
||||
@@ -722,7 +695,7 @@ func (m *v28Migrator) getActiveThreshold(value float64, steps []interface{}) map
|
||||
}
|
||||
|
||||
// parseNumericValue converts various types to float64 for threshold calculations
|
||||
func (m *v28Migrator) parseNumericValue(value interface{}) (float64, error) {
|
||||
func parseNumericValue(value interface{}) (float64, error) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return strconv.ParseFloat(v, 64)
|
||||
@@ -743,7 +716,7 @@ func (m *v28Migrator) parseNumericValue(value interface{}) (float64, error) {
|
||||
|
||||
// cleanupAngularProperties removes old angular properties after migration
|
||||
// Based on PanelModel.clearPropertiesBeforePluginChange in public/app/features/dashboard/state/PanelModel.ts
|
||||
func (m *v28Migrator) cleanupAngularProperties(panel map[string]interface{}) {
|
||||
func cleanupAngularProperties(panel map[string]interface{}) {
|
||||
// Remove PanelModel's autoMigrateFrom property
|
||||
delete(panel, "autoMigrateFrom")
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil"
|
||||
)
|
||||
|
||||
func TestV28(t *testing.T) {
|
||||
@@ -88,7 +87,8 @@ func TestV28(t *testing.T) {
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
|
||||
"pluginVersion": pluginVersionForAutoMigrate,
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
@@ -176,7 +176,7 @@ func TestV28(t *testing.T) {
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": pluginVersionForAutoMigrate,
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
@@ -257,7 +257,7 @@ func TestV28(t *testing.T) {
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": pluginVersionForAutoMigrate,
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
@@ -324,7 +324,7 @@ func TestV28(t *testing.T) {
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": pluginVersionForAutoMigrate,
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
@@ -391,7 +391,7 @@ func TestV28(t *testing.T) {
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": pluginVersionForAutoMigrate,
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
@@ -491,7 +491,7 @@ func TestV28(t *testing.T) {
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": pluginVersionForAutoMigrate,
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
@@ -556,7 +556,7 @@ func TestV28(t *testing.T) {
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": pluginVersionForAutoMigrate,
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
@@ -621,7 +621,7 @@ func TestV28(t *testing.T) {
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": pluginVersionForAutoMigrate,
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
@@ -696,7 +696,7 @@ func TestV28(t *testing.T) {
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": pluginVersionForAutoMigrate,
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
@@ -746,46 +746,5 @@ func TestV28(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
errorTests := []migrationTestCase{
|
||||
{
|
||||
name: "throw an error if stat panel plugin not found",
|
||||
input: map[string]interface{}{
|
||||
"schemaVersion": 27,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"type": "singlestat",
|
||||
"valueName": "avg",
|
||||
"format": "ms",
|
||||
"decimals": 2,
|
||||
"thresholds": "10,20,30",
|
||||
"colors": []interface{}{"green", "yellow", "red"},
|
||||
"gauge": map[string]interface{}{
|
||||
"show": false,
|
||||
},
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "var1",
|
||||
"tags": []interface{}{"tag1"},
|
||||
"tagsQuery": "query",
|
||||
"tagValuesQuery": "values",
|
||||
"useTags": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "schema migration from version 28 to 41 failed: stat panel plugin not found when migrating dashboard to schema version 28",
|
||||
},
|
||||
}
|
||||
|
||||
runMigrationTests(t, tests, schemaversion.V28(testutil.GetTestPanelProvider()))
|
||||
runMigrationTests(t, errorTests, schemaversion.V28(testutil.GetTestPanelProviderWithCustomPanels([]schemaversion.PanelPluginInfo{
|
||||
{ID: "fake-plugin", Version: "1.0.0"},
|
||||
})))
|
||||
runMigrationTests(t, tests, schemaversion.V28)
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -90,4 +90,4 @@
|
||||
"refresh": "",
|
||||
"schemaVersion": 41,
|
||||
"title": "V22 Table Panel Styles Test"
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -218,7 +218,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -283,7 +283,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -362,7 +362,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -433,7 +433,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -503,7 +503,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -574,7 +574,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -664,7 +664,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -770,7 +770,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -870,7 +870,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -983,7 +983,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1102,7 +1102,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1215,7 +1215,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1353,7 +1353,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -1370,4 +1370,4 @@
|
||||
],
|
||||
"refresh": "",
|
||||
"schemaVersion": 41
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -51,7 +51,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -123,7 +123,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -221,7 +221,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -120,7 +120,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -193,7 +193,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -292,7 +292,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -379,7 +379,7 @@
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"pluginVersion": "12.1.0",
|
||||
"postfix": "b",
|
||||
"postfixFontSize": "50%",
|
||||
"prefix": "a",
|
||||
|
||||
@@ -8,10 +8,6 @@ import (
|
||||
|
||||
type TestDataSourceProvider struct{}
|
||||
|
||||
type TestPanelProvider struct {
|
||||
customPanels []schemaversion.PanelPluginInfo
|
||||
}
|
||||
|
||||
func (m *TestDataSourceProvider) GetDataSourceInfo(_ context.Context) []schemaversion.DataSourceInfo {
|
||||
return []schemaversion.DataSourceInfo{
|
||||
{
|
||||
@@ -65,54 +61,7 @@ func (m *TestDataSourceProvider) GetDataSourceInfo(_ context.Context) []schemave
|
||||
}
|
||||
}
|
||||
|
||||
func (m *TestPanelProvider) GetPanels() []schemaversion.PanelPluginInfo {
|
||||
if len(m.customPanels) > 0 {
|
||||
return m.customPanels
|
||||
}
|
||||
|
||||
// Default panels
|
||||
return []schemaversion.PanelPluginInfo{
|
||||
{
|
||||
ID: "gauge",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
{
|
||||
ID: "stat",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
{
|
||||
ID: "table",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
// Note: grafana-singlestat-panel is not included to match frontend test environment
|
||||
// This ensures both frontend and backend migrations produce the same result
|
||||
}
|
||||
}
|
||||
|
||||
func (m *TestPanelProvider) GetPanelPlugin(id string) schemaversion.PanelPluginInfo {
|
||||
// check if it exists in the list of mocked panels
|
||||
for _, panel := range m.GetPanels() {
|
||||
if panel.ID == id {
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
return schemaversion.PanelPluginInfo{}
|
||||
}
|
||||
|
||||
// GetTestDataSourceProvider returns a singleton instance of the test provider
|
||||
func GetTestDataSourceProvider() *TestDataSourceProvider {
|
||||
return &TestDataSourceProvider{}
|
||||
}
|
||||
|
||||
// GetTestPanelProvider returns a singleton instance of the test panel provider
|
||||
func GetTestPanelProvider() *TestPanelProvider {
|
||||
return &TestPanelProvider{}
|
||||
}
|
||||
|
||||
// GetTestPanelProviderWithCustomPanels returns a test panel provider with custom panels
|
||||
func GetTestPanelProviderWithCustomPanels(customPanels []schemaversion.PanelPluginInfo) *TestPanelProvider {
|
||||
return &TestPanelProvider{
|
||||
customPanels: customPanels,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
)
|
||||
|
||||
func TestDashboardAPIBuilder_Mutate(t *testing.T) {
|
||||
migration.Initialize(testutil.GetTestDataSourceProvider(), testutil.GetTestPanelProvider())
|
||||
migration.Initialize(testutil.GetTestDataSourceProvider())
|
||||
tests := []struct {
|
||||
name string
|
||||
inputObj runtime.Object
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
)
|
||||
|
||||
type PluginStorePanelProvider struct {
|
||||
pluginStore pluginstore.Store
|
||||
buildVersion string
|
||||
}
|
||||
|
||||
func (p *PluginStorePanelProvider) GetPanels() []schemaversion.PanelPluginInfo {
|
||||
panelPlugins := p.pluginStore.Plugins(context.Background(), plugins.TypePanel)
|
||||
|
||||
panels := make([]schemaversion.PanelPluginInfo, len(panelPlugins))
|
||||
for i, plugin := range panelPlugins {
|
||||
version := plugin.Info.Version
|
||||
if version == "" {
|
||||
version = p.buildVersion
|
||||
}
|
||||
panels[i] = schemaversion.PanelPluginInfo{
|
||||
ID: plugin.ID,
|
||||
Version: version,
|
||||
}
|
||||
}
|
||||
return panels
|
||||
}
|
||||
|
||||
func (p *PluginStorePanelProvider) GetPanelPlugin(id string) schemaversion.PanelPluginInfo {
|
||||
for _, plugin := range p.GetPanels() {
|
||||
if plugin.ID == id {
|
||||
return plugin
|
||||
}
|
||||
}
|
||||
|
||||
return schemaversion.PanelPluginInfo{}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPluginStorePanelProvider_GetPanels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
plugins []pluginstore.Plugin
|
||||
buildVersion string
|
||||
expectedPanels []schemaversion.PanelPluginInfo
|
||||
}{
|
||||
{
|
||||
name: "should return all panel plugins with their versions",
|
||||
plugins: []pluginstore.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{ID: "gauge", Info: plugins.Info{Version: "1.0.0"}},
|
||||
},
|
||||
{
|
||||
JSONData: plugins.JSONData{ID: "stat", Info: plugins.Info{Version: "2.0.0"}},
|
||||
},
|
||||
{
|
||||
JSONData: plugins.JSONData{ID: "timeseries", Info: plugins.Info{Version: "3.0.0"}},
|
||||
},
|
||||
},
|
||||
buildVersion: "10.0.0",
|
||||
expectedPanels: []schemaversion.PanelPluginInfo{
|
||||
{ID: "gauge", Version: "1.0.0"},
|
||||
{ID: "stat", Version: "2.0.0"},
|
||||
{ID: "timeseries", Version: "3.0.0"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should use build version when plugin version is empty",
|
||||
plugins: []pluginstore.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{ID: "gauge", Info: plugins.Info{Version: ""}},
|
||||
},
|
||||
{
|
||||
JSONData: plugins.JSONData{ID: "stat", Info: plugins.Info{Version: "2.0.0"}},
|
||||
},
|
||||
},
|
||||
buildVersion: "10.0.0",
|
||||
expectedPanels: []schemaversion.PanelPluginInfo{
|
||||
{ID: "gauge", Version: "10.0.0"},
|
||||
{ID: "stat", Version: "2.0.0"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should return empty slice when no plugins",
|
||||
plugins: []pluginstore.Plugin{},
|
||||
buildVersion: "10.0.0",
|
||||
expectedPanels: []schemaversion.PanelPluginInfo{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create mock plugin store
|
||||
mockStore := &mockPluginStore{
|
||||
plugins: tt.plugins,
|
||||
}
|
||||
|
||||
// Create mock setting
|
||||
mockSetting := &setting.Cfg{
|
||||
BuildVersion: tt.buildVersion,
|
||||
}
|
||||
|
||||
// Create provider
|
||||
provider := &PluginStorePanelProvider{
|
||||
pluginStore: mockStore,
|
||||
buildVersion: mockSetting.BuildVersion,
|
||||
}
|
||||
|
||||
// Call the function
|
||||
result := provider.GetPanels()
|
||||
|
||||
// Assert results
|
||||
assert.Len(t, result, len(tt.expectedPanels))
|
||||
for i, expected := range tt.expectedPanels {
|
||||
assert.Equal(t, expected.ID, result[i].ID)
|
||||
assert.Equal(t, expected.Version, result[i].Version)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginStorePanelProvider_GetPanelPlugin(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
plugins []pluginstore.Plugin
|
||||
buildVersion string
|
||||
searchID string
|
||||
expectedPanel schemaversion.PanelPluginInfo
|
||||
}{
|
||||
{
|
||||
name: "should return panel plugin when found",
|
||||
plugins: []pluginstore.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{ID: "gauge", Info: plugins.Info{Version: "1.0.0"}},
|
||||
},
|
||||
{
|
||||
JSONData: plugins.JSONData{ID: "stat", Info: plugins.Info{Version: "2.0.0"}},
|
||||
},
|
||||
},
|
||||
buildVersion: "10.0.0",
|
||||
searchID: "stat",
|
||||
expectedPanel: schemaversion.PanelPluginInfo{
|
||||
ID: "stat",
|
||||
Version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should return panel plugin with build version when plugin version is empty",
|
||||
plugins: []pluginstore.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{ID: "gauge", Info: plugins.Info{Version: ""}},
|
||||
},
|
||||
},
|
||||
buildVersion: "10.0.0",
|
||||
searchID: "gauge",
|
||||
expectedPanel: schemaversion.PanelPluginInfo{
|
||||
ID: "gauge",
|
||||
Version: "10.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should return empty panel plugin when not found",
|
||||
plugins: []pluginstore.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{ID: "gauge", Info: plugins.Info{Version: "1.0.0"}},
|
||||
},
|
||||
},
|
||||
buildVersion: "10.0.0",
|
||||
searchID: "nonexistent",
|
||||
expectedPanel: schemaversion.PanelPluginInfo{},
|
||||
},
|
||||
{
|
||||
name: "should return empty panel plugin when no plugins exist",
|
||||
plugins: []pluginstore.Plugin{},
|
||||
buildVersion: "10.0.0",
|
||||
searchID: "gauge",
|
||||
expectedPanel: schemaversion.PanelPluginInfo{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockStore := &mockPluginStore{
|
||||
plugins: tt.plugins,
|
||||
}
|
||||
|
||||
mockSetting := &setting.Cfg{
|
||||
BuildVersion: tt.buildVersion,
|
||||
}
|
||||
|
||||
provider := &PluginStorePanelProvider{
|
||||
pluginStore: mockStore,
|
||||
buildVersion: mockSetting.BuildVersion,
|
||||
}
|
||||
|
||||
result := provider.GetPanelPlugin(tt.searchID)
|
||||
|
||||
assert.Equal(t, tt.expectedPanel.ID, result.ID)
|
||||
assert.Equal(t, tt.expectedPanel.Version, result.Version)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockPluginStore struct {
|
||||
plugins []pluginstore.Plugin
|
||||
}
|
||||
|
||||
func (m *mockPluginStore) Plugin(ctx context.Context, pluginID string) (pluginstore.Plugin, bool) {
|
||||
for _, p := range m.plugins {
|
||||
if p.ID == pluginID {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
return pluginstore.Plugin{}, false
|
||||
}
|
||||
|
||||
func (m *mockPluginStore) Plugins(ctx context.Context, pluginTypes ...plugins.Type) []pluginstore.Plugin {
|
||||
return m.plugins
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"maps"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -28,6 +27,8 @@ import (
|
||||
dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1"
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration"
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/conversion"
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||
@@ -37,8 +38,10 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
authsvc "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/client"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service"
|
||||
@@ -56,10 +59,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
|
||||
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/client"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -167,9 +166,6 @@ func RegisterAPIService(
|
||||
migration.RegisterMetrics(reg)
|
||||
migration.Initialize(&datasourceInfoProvider{
|
||||
datasourceService: datasourceService,
|
||||
}, &PluginStorePanelProvider{
|
||||
pluginStore: pluginStore,
|
||||
buildVersion: cfg.BuildVersion,
|
||||
})
|
||||
apiregistration.RegisterAPI(builder)
|
||||
return builder
|
||||
@@ -184,10 +180,7 @@ func NewAPIService(ac claims.AccessClient, features featuremgmt.FeatureToggles,
|
||||
|
||||
logger := log.New("grafana-apiserver.dashboards")
|
||||
|
||||
migration.Initialize(datasourceProvider, &PluginStorePanelProvider{
|
||||
pluginStore: pluginStore,
|
||||
buildVersion: "unknown",
|
||||
})
|
||||
migration.Initialize(datasourceProvider)
|
||||
|
||||
return &DashboardsAPIBuilder{
|
||||
log: logger,
|
||||
|
||||
@@ -75,6 +75,8 @@ const dataSources = {
|
||||
}),
|
||||
};
|
||||
|
||||
const pluginVersionForAutoMigrate = '12.1.0';
|
||||
|
||||
describe('Backend / Frontend result comparison', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -157,11 +159,11 @@ describe('Backend / Frontend result comparison', () => {
|
||||
logos: { small: 'small/logo', large: 'large/logo' },
|
||||
screenshots: [],
|
||||
updated: '2024-01-01',
|
||||
version: '1.0.0',
|
||||
version: pluginVersionForAutoMigrate,
|
||||
};
|
||||
}
|
||||
if (!statPanelPlugin.meta.info.version) {
|
||||
statPanelPlugin.meta.info.version = '1.0.0';
|
||||
statPanelPlugin.meta.info.version = pluginVersionForAutoMigrate;
|
||||
}
|
||||
|
||||
await panel.pluginLoaded(statPanelPlugin);
|
||||
@@ -179,11 +181,11 @@ describe('Backend / Frontend result comparison', () => {
|
||||
logos: { small: 'small/logo', large: 'large/logo' },
|
||||
screenshots: [],
|
||||
updated: '2024-01-01',
|
||||
version: '1.0.0',
|
||||
version: pluginVersionForAutoMigrate,
|
||||
};
|
||||
}
|
||||
if (!tablePanelPlugin.meta.info.version) {
|
||||
tablePanelPlugin.meta.info.version = '1.0.0';
|
||||
tablePanelPlugin.meta.info.version = pluginVersionForAutoMigrate;
|
||||
}
|
||||
|
||||
await panel.pluginLoaded(tablePanelPlugin as any);
|
||||
|
||||
Reference in New Issue
Block a user