mirror of https://github.com/grafana/grafana.git
448 lines
12 KiB
Go
448 lines
12 KiB
Go
package schemaversion
|
|
|
|
import (
|
|
"context"
|
|
"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(_ context.Context, 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
|
|
}
|
|
|
|
// Check if all mappings are already in the new format
|
|
if areAllMappingsNewFormat(oldMappings) {
|
|
return oldMappings
|
|
}
|
|
|
|
valueMaps := createValueMaps()
|
|
var newMappings []interface{}
|
|
hasValueMappings := false
|
|
|
|
for _, mapping := range oldMappings {
|
|
if mappingMap, ok := mapping.(map[string]interface{}); ok {
|
|
if isNewFormatMapping(mappingMap) {
|
|
hasValueMappings = processNewFormatMapping(mappingMap, valueMaps, &newMappings, hasValueMappings)
|
|
} else {
|
|
hasValueMappings = processLegacyMapping(mappingMap, thresholds, valueMaps, &newMappings, hasValueMappings)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add consolidated value mappings at the beginning if any exist
|
|
if hasValueMappings {
|
|
newMappings = append([]interface{}{valueMaps}, newMappings...)
|
|
}
|
|
|
|
return newMappings
|
|
}
|
|
|
|
// areAllMappingsNewFormat checks if all mappings are already in the new format
|
|
func areAllMappingsNewFormat(oldMappings []interface{}) bool {
|
|
for _, mapping := range oldMappings {
|
|
if mappingMap, ok := mapping.(map[string]interface{}); ok {
|
|
if mappingType, ok := mappingMap["type"].(string); ok && mappingType != "" {
|
|
// This is already in new format, keep it as-is
|
|
continue
|
|
} else {
|
|
// Found a legacy format mapping, need to process
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// createValueMaps creates the base value maps structure
|
|
func createValueMaps() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"type": "value",
|
|
"options": map[string]interface{}{},
|
|
}
|
|
}
|
|
|
|
// isNewFormatMapping checks if a mapping is already in the new format
|
|
func isNewFormatMapping(mappingMap map[string]interface{}) bool {
|
|
if mappingType, ok := mappingMap["type"].(string); ok && mappingType != "" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// processNewFormatMapping handles mappings that are already in the new format
|
|
func processNewFormatMapping(mappingMap map[string]interface{}, valueMaps map[string]interface{}, newMappings *[]interface{}, hasValueMappings bool) bool {
|
|
mappingType := mappingMap["type"].(string)
|
|
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)
|
|
}
|
|
return hasValueMappings
|
|
}
|
|
|
|
// processLegacyMapping handles legacy format mappings
|
|
func processLegacyMapping(mappingMap map[string]interface{}, thresholds map[string]interface{}, valueMaps map[string]interface{}, newMappings *[]interface{}, hasValueMappings bool) bool {
|
|
color := getColorFromThresholds(mappingMap, thresholds)
|
|
|
|
// Convert legacy type numbers to new format
|
|
if mappingType := GetIntValue(mappingMap, "type", -1); mappingType != -1 {
|
|
switch mappingType {
|
|
case 1: // ValueToText
|
|
hasValueMappings = processValueToTextMapping(mappingMap, color, thresholds, valueMaps, newMappings, hasValueMappings)
|
|
case 2: // RangeToText
|
|
processRangeToTextMapping(mappingMap, color, newMappings)
|
|
}
|
|
}
|
|
|
|
return hasValueMappings
|
|
}
|
|
|
|
// getColorFromThresholds extracts color from thresholds based on mapping text (matches frontend behavior)
|
|
func getColorFromThresholds(mappingMap map[string]interface{}, thresholds map[string]interface{}) interface{} {
|
|
if thresholds == nil {
|
|
return nil
|
|
}
|
|
|
|
// Try to get color from threshold based on the mapping text (matches frontend behavior)
|
|
if text, ok := mappingMap["text"]; ok {
|
|
if textStr, ok := text.(string); ok {
|
|
if numeric, err := strconv.ParseFloat(textStr, 64); err == nil {
|
|
return getActiveThresholdColor(numeric, thresholds)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// processValueToTextMapping handles ValueToText legacy mappings
|
|
func processValueToTextMapping(mappingMap map[string]interface{}, color interface{}, thresholds map[string]interface{}, valueMaps map[string]interface{}, newMappings *[]interface{}, hasValueMappings bool) bool {
|
|
if value, ok := mappingMap["value"]; ok {
|
|
if valueStr, ok := value.(string); ok && valueStr == "null" {
|
|
// Handle null values as special value mapping
|
|
processNullValueMapping(mappingMap, color, thresholds, newMappings)
|
|
} else {
|
|
// Regular value mapping
|
|
processRegularValueMapping(mappingMap, value, color, valueMaps)
|
|
hasValueMappings = true
|
|
}
|
|
}
|
|
return hasValueMappings
|
|
}
|
|
|
|
// processNullValueMapping creates a special value mapping for null values
|
|
func processNullValueMapping(mappingMap map[string]interface{}, color interface{}, thresholds map[string]interface{}, newMappings *[]interface{}) {
|
|
result := map[string]interface{}{
|
|
"text": mappingMap["text"],
|
|
}
|
|
if color != nil {
|
|
result["color"] = color
|
|
}
|
|
|
|
*newMappings = append(*newMappings, map[string]interface{}{
|
|
"type": "special",
|
|
"options": map[string]interface{}{
|
|
"match": "null",
|
|
"result": result,
|
|
},
|
|
})
|
|
}
|
|
|
|
// processRegularValueMapping creates a regular value mapping
|
|
func processRegularValueMapping(mappingMap map[string]interface{}, value interface{}, color interface{}, valueMaps map[string]interface{}) {
|
|
valueMapsOptions := valueMaps["options"].(map[string]interface{})
|
|
result := map[string]interface{}{
|
|
"text": mappingMap["text"],
|
|
}
|
|
if color != nil {
|
|
result["color"] = color
|
|
}
|
|
valueMapsOptions[stringifyValue(value)] = result
|
|
}
|
|
|
|
// processRangeToTextMapping handles RangeToText legacy mappings
|
|
func processRangeToTextMapping(mappingMap map[string]interface{}, color interface{}, newMappings *[]interface{}) {
|
|
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,
|
|
},
|
|
})
|
|
}
|
|
|
|
// getActiveThresholdColor returns the color for a value based on thresholds (matches frontend getActiveThreshold)
|
|
func getActiveThresholdColor(value float64, thresholds map[string]interface{}) interface{} {
|
|
if steps, ok := thresholds["steps"].([]interface{}); ok {
|
|
if len(steps) == 0 {
|
|
return nil
|
|
}
|
|
|
|
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 := GetFloatValue(stepMap, "value", -1); stepNum != -1 {
|
|
if value >= stepNum {
|
|
activeStep = stepMap
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if activeStep != nil {
|
|
return activeStep["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 ""
|
|
}
|
|
}
|