diff --git a/apps/dashboard/pkg/migration/schemaversion/migrations.go b/apps/dashboard/pkg/migration/schemaversion/migrations.go index 296e892d573..e8ae5ba6140 100644 --- a/apps/dashboard/pkg/migration/schemaversion/migrations.go +++ b/apps/dashboard/pkg/migration/schemaversion/migrations.go @@ -5,7 +5,7 @@ import ( ) const ( - MIN_VERSION = 30 + MIN_VERSION = 29 LATEST_VERSION = 41 ) @@ -26,6 +26,7 @@ type DataSourceInfoProvider interface { func GetMigrations(dsInfoProvider DataSourceInfoProvider) map[int]SchemaVersionMigrationFunc { return map[int]SchemaVersionMigrationFunc{ + 30: V30, 31: V31, 32: V32, 33: V33(dsInfoProvider), diff --git a/apps/dashboard/pkg/migration/schemaversion/v30.go b/apps/dashboard/pkg/migration/schemaversion/v30.go new file mode 100644 index 00000000000..d892abecb50 --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v30.go @@ -0,0 +1,393 @@ +package schemaversion + +import ( + "strconv" +) + +// V30 upgrades value mappings and migrates tooltip options for panels. +// +// This migration addresses two key improvements to panel configurations: +// 1. Value mappings upgrade: Converts legacy value mapping format to the new structured format +// 2. Tooltip options migration: Renames tooltipOptions to tooltip in specific panel types: timeseries, xychart, xychart2 +// +// The migration works by: +// 1. Iterating through all panels in the dashboard, including nested panels in collapsed rows +// 2. For each panel, upgrading value mappings in fieldConfig.defaults and fieldConfig.overrides +// 3. Migrating tooltip options for timeseries, xychart, and xychart2 panels +// 4. Preserving all other panel configurations and options +// +// Value Mappings Migration: +// - Converts legacy mapping types (1 = ValueToText, 2 = RangeToText) to new format +// - Handles special "null" values by converting them to SpecialValue mappings +// - Preserves threshold colors when available +// - Consolidates multiple value mappings into a single ValueToText mapping +// - Maintains range mappings as separate RangeToText mappings +// +// Tooltip Options Migration: +// - Renames panel.options.tooltipOptions to panel.options.tooltip +// - Only applies to timeseries, xychart, and xychart2 panel types +// - Preserves all tooltip configuration options +// +// Example value mappings transformation: +// +// Before migration: +// +// panel: { +// "fieldConfig": { +// "defaults": { +// "mappings": [ +// { "id": 0, "text": "Up", "type": 1, "value": "1" }, +// { "id": 1, "text": "Down", "type": 1, "value": "0" }, +// { "from": "10", "to": "20", "text": "Medium", "type": 2 } +// ] +// } +// } +// } +// +// After migration: +// +// panel: { +// "fieldConfig": { +// "defaults": { +// "mappings": [ +// { +// "type": "value", +// "options": { +// "1": { "text": "Up" }, +// "0": { "text": "Down" } +// } +// }, +// { +// "type": "range", +// "options": { +// "from": 10, +// "to": 20, +// "result": { "text": "Medium" } +// } +// } +// ] +// } +// } +// } +// +// Example tooltip options transformation: +// +// Before migration: +// +// panel: { +// "type": "timeseries", +// "options": { +// "tooltipOptions": { "mode": "multi" } +// } +// } +// +// After migration: +// +// panel: { +// "type": "timeseries", +// "options": { +// "tooltip": { "mode": "multi" } +// } +// } +func V30(dashboard map[string]interface{}) error { + dashboard["schemaVersion"] = 30 + + panels, ok := dashboard["panels"].([]interface{}) + if !ok { + return nil + } + + for _, panel := range panels { + panelMap, ok := panel.(map[string]interface{}) + if !ok { + continue + } + + upgradeValueMappingsForPanel(panelMap) + migrateTooltipOptions(panelMap) + + // Handle nested panels in collapsed rows + nestedPanels, hasNested := panelMap["panels"].([]interface{}) + if !hasNested { + continue + } + + for _, nestedPanel := range nestedPanels { + nestedPanelMap, ok := nestedPanel.(map[string]interface{}) + if !ok { + continue + } + upgradeValueMappingsForPanel(nestedPanelMap) + migrateTooltipOptions(nestedPanelMap) + } + } + + return nil +} + +// upgradeValueMappingsForPanel migrates value mappings from old format to new format +func upgradeValueMappingsForPanel(panel map[string]interface{}) { + fieldConfig, ok := panel["fieldConfig"].(map[string]interface{}) + if !ok { + return + } + + // Upgrade defaults mappings + if defaults, ok := fieldConfig["defaults"].(map[string]interface{}); ok { + if mappings, ok := defaults["mappings"].([]interface{}); ok { + var thresholds map[string]interface{} + if t, ok := defaults["thresholds"].(map[string]interface{}); ok { + thresholds = t + } + defaults["mappings"] = upgradeValueMappings(mappings, thresholds) + } + } + + // Upgrade overrides mappings + overrides, hasOverrides := fieldConfig["overrides"].([]interface{}) + if !hasOverrides { + return + } + + for _, override := range overrides { + overrideMap, ok := override.(map[string]interface{}) + if !ok { + continue + } + + properties, hasProperties := overrideMap["properties"].([]interface{}) + if !hasProperties { + continue + } + + for _, property := range properties { + propertyMap, ok := property.(map[string]interface{}) + if !ok { + continue + } + + if propertyMap["id"] != "mappings" { + continue + } + + mappings, ok := propertyMap["value"].([]interface{}) + if !ok { + continue + } + + propertyMap["value"] = upgradeValueMappings(mappings, nil) + } + } +} + +// upgradeValueMappings converts legacy value mappings to new format +func upgradeValueMappings(oldMappings []interface{}, thresholds map[string]interface{}) []interface{} { + if len(oldMappings) == 0 { + return oldMappings + } + + valueMaps := map[string]interface{}{ + "type": "value", + "options": map[string]interface{}{}, + } + + var newMappings []interface{} + hasValueMappings := false + + for _, mapping := range oldMappings { + if mappingMap, ok := mapping.(map[string]interface{}); ok { + // Check if this is already the new format + if mappingType, ok := mappingMap["type"].(string); ok && mappingType != "" { + if mappingType == "value" { + // Consolidate existing value mappings + if options, ok := mappingMap["options"].(map[string]interface{}); ok { + valueMapsOptions := valueMaps["options"].(map[string]interface{}) + for k, v := range options { + valueMapsOptions[k] = v + } + hasValueMappings = true + } + } else { + newMappings = append(newMappings, mappingMap) + } + continue + } + + // Handle legacy format + var color interface{} + if thresholds != nil { + // Try to get color from threshold based on the mapping value + if value, ok := mappingMap["value"]; ok { + if valueStr, ok := value.(string); ok { + if numeric, err := strconv.ParseFloat(valueStr, 64); err == nil { + color = getActiveThresholdColor(numeric, thresholds) + } + } + } + // For range mappings, use the 'from' value to determine color + if fromVal, ok := mappingMap["from"]; ok { + if fromStr, ok := fromVal.(string); ok { + if numeric, err := strconv.ParseFloat(fromStr, 64); err == nil { + color = getActiveThresholdColor(numeric, thresholds) + } + } + } + } + + // Convert legacy type numbers to new format + if mappingType, ok := mappingMap["type"].(float64); ok { + switch int(mappingType) { + case 1: // ValueToText + if value, ok := mappingMap["value"]; ok { + if valueStr, ok := value.(string); ok && valueStr == "null" { + // Handle null values as special value mapping + // For null values, use the base threshold color (lowest step) + if thresholds != nil { + color = getBaseThresholdColor(thresholds) + } + newMappings = append(newMappings, map[string]interface{}{ + "type": "special", + "options": map[string]interface{}{ + "match": "null", + "result": map[string]interface{}{ + "text": mappingMap["text"], + "color": color, + }, + }, + }) + } else { + // Regular value mapping + valueMapsOptions := valueMaps["options"].(map[string]interface{}) + result := map[string]interface{}{ + "text": mappingMap["text"], + } + if color != nil { + result["color"] = color + } + valueMapsOptions[stringifyValue(value)] = result + hasValueMappings = true + } + } + case 2: // RangeToText + result := map[string]interface{}{ + "text": mappingMap["text"], + } + if color != nil { + result["color"] = color + } + + from, _ := strconv.ParseFloat(stringifyValue(mappingMap["from"]), 64) + to, _ := strconv.ParseFloat(stringifyValue(mappingMap["to"]), 64) + + newMappings = append(newMappings, map[string]interface{}{ + "type": "range", + "options": map[string]interface{}{ + "from": from, + "to": to, + "result": result, + }, + }) + } + } + } + } + + // Add consolidated value mappings at the beginning if any exist + if hasValueMappings { + newMappings = append([]interface{}{valueMaps}, newMappings...) + } + + return newMappings +} + +// getActiveThresholdColor returns the color for a value based on thresholds +func getActiveThresholdColor(value float64, thresholds map[string]interface{}) interface{} { + if steps, ok := thresholds["steps"].([]interface{}); ok { + var activeStep map[string]interface{} + + for _, step := range steps { + if stepMap, ok := step.(map[string]interface{}); ok { + if stepValue, ok := stepMap["value"]; ok { + if stepValue == nil { + // Null value represents negative infinity - this is always the base color + activeStep = stepMap + continue + } + + if stepNum, ok := stepValue.(float64); ok { + if value >= stepNum { + activeStep = stepMap + } + } + } + } + } + + if activeStep != nil { + return activeStep["color"] + } + } + + return nil +} + +// getBaseThresholdColor returns the base threshold color (first step with null value) +func getBaseThresholdColor(thresholds map[string]interface{}) interface{} { + if steps, ok := thresholds["steps"].([]interface{}); ok { + for _, step := range steps { + if stepMap, ok := step.(map[string]interface{}); ok { + if stepValue, ok := stepMap["value"]; ok { + if stepValue == nil { + // This is the base color (null value step) + return stepMap["color"] + } + } + } + } + } + + return nil +} + +// migrateTooltipOptions renames tooltipOptions to tooltip for specific panel types +func migrateTooltipOptions(panel map[string]interface{}) { + panelType, ok := panel["type"].(string) + if !ok { + return + } + + // Only migrate for specific panel types + if panelType != "timeseries" && panelType != "xychart" && panelType != "xychart2" { + return + } + + options, ok := panel["options"].(map[string]interface{}) + if !ok { + return + } + + tooltipOptions, ok := options["tooltipOptions"] + if !ok { + return + } + + // Rename tooltipOptions to tooltip + options["tooltip"] = tooltipOptions + delete(options, "tooltipOptions") +} + +// stringifyValue converts a value to string +func stringifyValue(value interface{}) string { + switch v := value.(type) { + case string: + return v + case float64: + return strconv.FormatFloat(v, 'f', -1, 64) + case int: + return strconv.Itoa(v) + case bool: + return strconv.FormatBool(v) + default: + return "" + } +} diff --git a/apps/dashboard/pkg/migration/schemaversion/v30_test.go b/apps/dashboard/pkg/migration/schemaversion/v30_test.go new file mode 100644 index 00000000000..bbae0e9a412 --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v30_test.go @@ -0,0 +1,499 @@ +package schemaversion_test + +import ( + "testing" + + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" +) + +func TestV30(t *testing.T) { + tests := []migrationTestCase{ + { + name: "panel with legacy value mappings gets upgraded to new format", + input: map[string]interface{}{ + "title": "V30 Value Mappings Migration Test Dashboard", + "schemaVersion": 29, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with legacy value mappings", + "id": 1, + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "thresholds": map[string]interface{}{ + "mode": "absolute", + "steps": []interface{}{ + map[string]interface{}{ + "color": "green", + "value": nil, + }, + map[string]interface{}{ + "color": "red", + "value": float64(80), + }, + }, + }, + "mappings": []interface{}{ + map[string]interface{}{ + "id": 0, + "text": "Up", + "type": float64(1), + "value": "1", + }, + map[string]interface{}{ + "id": 1, + "text": "Down", + "type": float64(1), + "value": "0", + }, + map[string]interface{}{ + "from": "10", + "to": "20", + "text": "Medium", + "type": float64(2), + }, + map[string]interface{}{ + "type": float64(1), + "value": "null", + "text": "Null Value", + }, + }, + }, + "overrides": []interface{}{ + map[string]interface{}{ + "matcher": map[string]interface{}{ + "id": "byName", + "options": "test-field", + }, + "properties": []interface{}{ + map[string]interface{}{ + "id": "mappings", + "value": []interface{}{ + map[string]interface{}{ + "id": 0, + "text": "Override Up", + "type": float64(1), + "value": "1", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V30 Value Mappings Migration Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with legacy value mappings", + "id": 1, + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "thresholds": map[string]interface{}{ + "mode": "absolute", + "steps": []interface{}{ + map[string]interface{}{ + "color": "green", + "value": nil, + }, + map[string]interface{}{ + "color": "red", + "value": float64(80), + }, + }, + }, + "mappings": []interface{}{ + map[string]interface{}{ + "type": "value", + "options": map[string]interface{}{ + "1": map[string]interface{}{ + "text": "Up", + "color": "green", + }, + "0": map[string]interface{}{ + "text": "Down", + "color": "green", + }, + }, + }, + map[string]interface{}{ + "type": "range", + "options": map[string]interface{}{ + "from": float64(10), + "to": float64(20), + "result": map[string]interface{}{ + "text": "Medium", + "color": "green", + }, + }, + }, + map[string]interface{}{ + "type": "special", + "options": map[string]interface{}{ + "match": "null", + "result": map[string]interface{}{ + "text": "Null Value", + "color": "green", + }, + }, + }, + }, + }, + "overrides": []interface{}{ + map[string]interface{}{ + "matcher": map[string]interface{}{ + "id": "byName", + "options": "test-field", + }, + "properties": []interface{}{ + map[string]interface{}{ + "id": "mappings", + "value": []interface{}{ + map[string]interface{}{ + "type": "value", + "options": map[string]interface{}{ + "1": map[string]interface{}{ + "text": "Override Up", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "panel with tooltip options gets migrated to tooltip", + input: map[string]interface{}{ + "title": "V30 Tooltip Options Migration Test Dashboard", + "schemaVersion": 29, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with tooltipOptions", + "id": 1, + "options": map[string]interface{}{ + "tooltipOptions": map[string]interface{}{ + "mode": "multi", + }, + }, + }, + map[string]interface{}{ + "type": "xychart", + "title": "XY Chart with tooltipOptions", + "id": 2, + "options": map[string]interface{}{ + "tooltipOptions": map[string]interface{}{ + "mode": "single", + }, + }, + }, + map[string]interface{}{ + "type": "xychart2", + "title": "XY Chart2 with tooltipOptions", + "id": 3, + "options": map[string]interface{}{ + "tooltipOptions": map[string]interface{}{ + "mode": "single", + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V30 Tooltip Options Migration Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with tooltipOptions", + "id": 1, + "options": map[string]interface{}{ + "tooltip": map[string]interface{}{ + "mode": "multi", + }, + }, + }, + map[string]interface{}{ + "type": "xychart", + "title": "XY Chart with tooltipOptions", + "id": 2, + "options": map[string]interface{}{ + "tooltip": map[string]interface{}{ + "mode": "single", + }, + }, + }, + map[string]interface{}{ + "type": "xychart2", + "title": "XY Chart2 with tooltipOptions", + "id": 3, + "options": map[string]interface{}{ + "tooltip": map[string]interface{}{ + "mode": "single", + }, + }, + }, + }, + }, + }, + { + name: "panel with nested panels in collapsed row gets migrated", + input: map[string]interface{}{ + "title": "V30 Nested Panels Migration Test Dashboard", + "schemaVersion": 29, + "panels": []interface{}{ + map[string]interface{}{ + "type": "row", + "title": "Collapsed Row", + "id": 1, + "collapsed": true, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Nested panel with tooltipOptions", + "id": 2, + "options": map[string]interface{}{ + "tooltipOptions": map[string]interface{}{ + "mode": "multi", + }, + }, + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "mappings": []interface{}{ + map[string]interface{}{ + "id": 0, + "text": "On", + "type": float64(1), + "value": "1", + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V30 Nested Panels Migration Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "row", + "title": "Collapsed Row", + "id": 1, + "collapsed": true, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Nested panel with tooltipOptions", + "id": 2, + "options": map[string]interface{}{ + "tooltip": map[string]interface{}{ + "mode": "multi", + }, + }, + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "mappings": []interface{}{ + map[string]interface{}{ + "type": "value", + "options": map[string]interface{}{ + "1": map[string]interface{}{ + "text": "On", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "panel with no mappings or tooltip options remains unchanged", + input: map[string]interface{}{ + "title": "V30 No Changes Test Dashboard", + "schemaVersion": 29, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with no relevant configurations", + "id": 1, + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "unit": "bytes", + }, + }, + "options": map[string]interface{}{ + "legend": map[string]interface{}{ + "displayMode": "list", + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V30 No Changes Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with no relevant configurations", + "id": 1, + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "unit": "bytes", + }, + }, + "options": map[string]interface{}{ + "legend": map[string]interface{}{ + "displayMode": "list", + }, + }, + }, + }, + }, + }, + { + name: "panels remain unchanged when no V30 specific migrations apply", + input: map[string]interface{}{ + "title": "V30 Panel Types Unchanged Test Dashboard", + "schemaVersion": 29, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "title": "Graph panel", + "id": 1, + }, + map[string]interface{}{ + "type": "singlestat", + "title": "Singlestat panel", + "id": 2, + }, + map[string]interface{}{ + "type": "timeseries", + "title": "Already timeseries panel", + "id": 3, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V30 Panel Types Unchanged Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "title": "Graph panel", + "id": 1, + }, + map[string]interface{}{ + "type": "singlestat", + "title": "Singlestat panel", + "id": 2, + }, + map[string]interface{}{ + "type": "timeseries", + "title": "Already timeseries panel", + "id": 3, + }, + }, + }, + }, + { + name: "graph panels with different configurations remain unchanged in V30", + input: map[string]interface{}{ + "title": "V30 Graph Panel Configurations Test Dashboard", + "schemaVersion": 29, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "title": "Graph with series mode and legend values", + "id": 1, + "xaxis": map[string]interface{}{ + "mode": "series", + }, + "legend": map[string]interface{}{ + "values": true, + }, + }, + map[string]interface{}{ + "type": "graph", + "title": "Graph with series mode", + "id": 2, + "xaxis": map[string]interface{}{ + "mode": "series", + }, + }, + map[string]interface{}{ + "type": "graph", + "title": "Graph with histogram mode", + "id": 3, + "xaxis": map[string]interface{}{ + "mode": "histogram", + }, + }, + map[string]interface{}{ + "type": "graph", + "title": "Graph with default configuration", + "id": 4, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V30 Graph Panel Configurations Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "title": "Graph with series mode and legend values", + "id": 1, + "xaxis": map[string]interface{}{ + "mode": "series", + }, + "legend": map[string]interface{}{ + "values": true, + }, + }, + map[string]interface{}{ + "type": "graph", + "title": "Graph with series mode", + "id": 2, + "xaxis": map[string]interface{}{ + "mode": "series", + }, + }, + map[string]interface{}{ + "type": "graph", + "title": "Graph with histogram mode", + "id": 3, + "xaxis": map[string]interface{}{ + "mode": "histogram", + }, + }, + map[string]interface{}{ + "type": "graph", + "title": "Graph with default configuration", + "id": 4, + }, + }, + }, + }, + } + + runMigrationTests(t, tests, schemaversion.V30) +} diff --git a/apps/dashboard/pkg/migration/testdata/input/v30.value_mappings_and_tooltip_options.json b/apps/dashboard/pkg/migration/testdata/input/v30.value_mappings_and_tooltip_options.json new file mode 100644 index 00000000000..672a88ed0b6 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/input/v30.value_mappings_and_tooltip_options.json @@ -0,0 +1,214 @@ +{ + "title": "V30 Value Mappings and Tooltip Options Migration Test Dashboard", + "schemaVersion": 29, + "panels": [ + { + "type": "timeseries", + "title": "Panel with legacy value mappings and tooltip options", + "id": 1, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "mappings": [ + { + "id": 0, + "text": "Up", + "type": 1, + "value": "1" + }, + { + "id": 1, + "text": "Down", + "type": 1, + "value": "0" + }, + { + "from": "10", + "to": "20", + "text": "Medium", + "type": 2 + }, + { + "type": 1, + "value": "null", + "text": "Null Value" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "test-field" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "id": 0, + "text": "Override Up", + "type": 1, + "value": "1" + } + ] + } + ] + } + ] + }, + "options": { + "tooltipOptions": { + "mode": "multi" + } + } + }, + { + "type": "xychart", + "title": "XY Chart with tooltip options only", + "id": 2, + "options": { + "tooltipOptions": { + "mode": "single" + } + } + }, + { + "type": "xychart2", + "title": "XY Chart2 with tooltip options", + "id": 3, + "options": { + "tooltipOptions": { + "mode": "single", + "sort": "none" + } + } + }, + { + "type": "graph", + "title": "Graph panel gets migrated to timeseries and tooltip", + "id": 4, + "options": { + "tooltip": { + "mode": "single" + } + } + }, + { + "type": "stat", + "title": "Panel with complex value mappings", + "id": 5, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "id": 0, + "text": "Critical", + "type": 1, + "value": "100" + }, + { + "from": "50", + "to": "99", + "text": "Warning", + "type": 2 + }, + { + "from": "0", + "to": "49", + "text": "OK", + "type": 2 + } + ] + } + } + }, + { + "type": "row", + "title": "Collapsed Row with nested panels", + "id": 6, + "collapsed": true, + "panels": [ + { + "type": "timeseries", + "title": "Nested panel with both migrations", + "id": 7, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "id": 0, + "text": "On", + "type": 1, + "value": "1" + }, + { + "id": 1, + "text": "Off", + "type": 1, + "value": "0" + } + ] + } + }, + "options": { + "tooltipOptions": { + "mode": "multi" + } + } + } + ] + }, + { + "type": "timeseries", + "title": "Panel with no relevant configurations", + "id": 8, + "fieldConfig": { + "defaults": { + "unit": "bytes" + } + }, + "options": { + "legend": { + "displayMode": "list" + } + } + }, + { + "type": "stat", + "title": "Panel with empty mappings array - should return null", + "id": 9, + "fieldConfig": { + "defaults": { + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "empty-field" + }, + "properties": [ + { + "id": "mappings", + "value": [] + } + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/output/v30.value_mappings_and_tooltip_options.json b/apps/dashboard/pkg/migration/testdata/output/v30.value_mappings_and_tooltip_options.json new file mode 100644 index 00000000000..508e41cb1f6 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/v30.value_mappings_and_tooltip_options.json @@ -0,0 +1,373 @@ +{ + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "0": { + "color": "green", + "text": "Down" + }, + "1": { + "color": "green", + "text": "Up" + } + }, + "type": "value" + }, + { + "options": { + "from": 10, + "result": { + "color": "green", + "text": "Medium" + }, + "to": 20 + }, + "type": "range" + }, + { + "options": { + "match": "null", + "result": { + "color": "green", + "text": "Null Value" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "test-field" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "1": { + "text": "Override Up" + } + }, + "type": "value" + } + ] + } + ] + } + ] + }, + "id": 1, + "options": { + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with legacy value mappings and tooltip options", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "options": { + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "XY Chart with tooltip options only", + "type": "xychart" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "options": { + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "XY Chart2 with tooltip options", + "type": "xychart2" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 4, + "options": { + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Graph panel gets migrated to timeseries and tooltip", + "type": "graph" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "100": { + "text": "Critical" + } + }, + "type": "value" + }, + { + "options": { + "from": 50, + "result": { + "text": "Warning" + }, + "to": 99 + }, + "type": "range" + }, + { + "options": { + "from": 0, + "result": { + "text": "OK" + }, + "to": 49 + }, + "type": "range" + } + ] + } + }, + "id": 5, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with complex value mappings", + "type": "stat" + }, + { + "collapsed": true, + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 6, + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "0": { + "text": "Off" + }, + "1": { + "text": "On" + } + }, + "type": "value" + } + ] + } + }, + "id": 7, + "options": { + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Nested panel with both migrations", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Collapsed Row with nested panels", + "type": "row" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "unit": "bytes" + } + }, + "id": 8, + "options": { + "legend": { + "displayMode": "list", + "showLegend": true + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with no relevant configurations", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "empty-field" + }, + "properties": [ + { + "id": "mappings", + "value": [] + } + ] + } + ] + }, + "id": 9, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with empty mappings array - should return null", + "type": "stat" + } + ], + "refresh": "", + "schemaVersion": 41, + "title": "V30 Value Mappings and Tooltip Options Migration Test Dashboard" +} \ No newline at end of file