Dashboard Migrations: V30 value mappings and tooltip options (#107970)
* Dashboard Migrations: V30 value mappings and tooltip options
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+214
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+373
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user