mirror of https://github.com/grafana/grafana
Dashboard Migrations: V30 value mappings and tooltip options (#107970)
* Dashboard Migrations: V30 value mappings and tooltip optionspull/108088/head^2
parent
b839090de5
commit
1200b684c6
@ -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 "" |
||||
} |
||||
} |
@ -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) |
||||
} |
@ -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": [] |
||||
} |
||||
] |
||||
} |
||||
] |
||||
} |
||||
} |
||||
] |
||||
} |
@ -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" |
||||
} |
Loading…
Reference in new issue