Dashboard: Backend always set metricEditorMode: 0 regardless metricQueryType and expression (#111613)

- Fix metricEditorMode logic to match frontend hasOwnProperty behavior
- Fix metricQueryType to only set when property doesn't exist
- Fix statistics processing to handle all types like frontend does
- Fix null statistic handling to match frontend behavior
- Fix object string representation to match JavaScript behavior
- Add comprehensive test coverage for all CloudWatch migration scenarios
- Achieve 100% backend-frontend consistency for v34 migration

* Fix v34 CloudWatch migration backend-frontend inconsistencies

- Fix metricEditorMode logic to match frontend hasOwnProperty behavior
- Fix metricQueryType to only set when property doesn't exist
- Fix statistics processing to handle all types like frontend does
- Fix null statistic handling to match frontend behavior
- Fix object string representation to match JavaScript behavior
- Add comprehensive test coverage for all CloudWatch migration scenarios
- Achieve 100% backend-frontend consistency for v34 migration

* Improve v34 CloudWatch migration test coverage

- Add targeted tests for hasOwnProperty logic coverage
- Add comprehensive getSuffixForStat coverage (all data types)
- Add generateNextRefId double letter generation tests
- Simplify test cases while maintaining full coverage
- Achieve 93%+ coverage on all critical v34 functions
- Remove redundant test cases for better maintainability

* Clean up v34 CloudWatch migration code

- Remove unnecessary comments and documentation
- Simplify function signatures and logic
- Maintain functionality while improving readability
- Keep essential logic intact with cleaner code

* Only remove unnecessary comments
This commit is contained in:
Ivan Ortega Alba
2025-09-29 11:35:58 +02:00
committed by GitHub
parent 512c292e04
commit 6d2fa6c583
5 changed files with 1297 additions and 153 deletions
@@ -1,6 +1,9 @@
package schemaversion
import "context"
import (
"context"
"fmt"
)
// V34 migrates CloudWatch queries that use multiple statistics into separate queries.
//
@@ -113,9 +116,24 @@ func migrateCloudWatchQueriesInPanel(panel map[string]interface{}) {
continue
}
// Add CloudWatch fields if missing (set to 0 if not present)
t["metricEditorMode"] = GetIntValue(t, "metricEditorMode", 0)
t["metricQueryType"] = GetIntValue(t, "metricQueryType", 0)
// Add CloudWatch fields if missing (matches frontend migrateCloudWatchQuery logic)
if _, hasMetricQueryType := t["metricQueryType"]; !hasMetricQueryType {
t["metricQueryType"] = 0 // MetricQueryType.Search
}
if _, hasMetricEditorMode := t["metricEditorMode"]; !hasMetricEditorMode {
metricQueryType := GetIntValue(t, "metricQueryType", 0)
if metricQueryType == 1 { // MetricQueryType.Insights
t["metricEditorMode"] = 1 // MetricEditorMode.Code
} else {
expression := GetStringValue(t, "expression")
if expression != "" {
t["metricEditorMode"] = 1 // MetricEditorMode.Code
} else {
t["metricEditorMode"] = 0 // MetricEditorMode.Builder
}
}
}
// Get valid statistics (including null and empty strings)
validStats, isEmpty := getValidStatistics(t["statistics"])
@@ -137,19 +155,20 @@ func migrateCloudWatchQueriesInPanel(panel map[string]interface{}) {
// No valid statistics - keep query as-is
newTargets = append(newTargets, t)
case 1:
// Single statistic - set statistic field if not null
if statString := GetStringValue(map[string]interface{}{"stat": validStats[0]}, "stat"); statString != "" {
t["statistic"] = statString
// Single statistic - set statistic field
// Frontend doesn't set statistic property for null values
if validStats[0] != nil {
t["statistic"] = validStats[0]
}
newTargets = append(newTargets, t)
default:
// Multiple statistics - create separate queries
for i, stat := range validStats {
newQuery := copyMap(t)
// Set statistic field
// Frontend doesn't set statistic property for null values
if stat != nil {
if statString, ok := stat.(string); ok {
newQuery["statistic"] = statString
}
newQuery["statistic"] = stat
}
if i == 0 {
@@ -210,10 +229,11 @@ func migrateCloudWatchAnnotationQueries(dashboard map[string]interface{}) {
delete(a, "statistics")
annotationsList[i] = a
case 1:
// Single statistic - set statistic field if not null
// Single statistic - set statistic field (matches frontend behavior)
delete(a, "statistics")
if statString := GetStringValue(map[string]interface{}{"stat": validStats[0]}, "stat"); statString != "" {
a["statistic"] = statString
// Frontend doesn't set statistic property for null values
if validStats[0] != nil {
a["statistic"] = validStats[0]
}
annotationsList[i] = a
default:
@@ -221,11 +241,10 @@ func migrateCloudWatchAnnotationQueries(dashboard map[string]interface{}) {
delete(a, "statistics")
for j, stat := range validStats {
newAnnotation := copyMap(a)
// Set statistic field (matches frontend behavior)
// Frontend doesn't set statistic property for null values
if stat != nil {
if statString, ok := stat.(string); ok {
newAnnotation["statistic"] = statString
}
newAnnotation["statistic"] = stat
}
// Add suffix to name
@@ -260,14 +279,9 @@ func getValidStatistics(statisticsField interface{}) ([]interface{}, bool) {
return nil, true // Return nil with true flag to indicate "empty array"
}
var valid []interface{}
for _, stat := range statistics {
// Include null and strings (including empty strings)
if stat == nil || isString(stat) {
valid = append(valid, stat)
}
}
return valid, false
// Frontend processes ALL values in statistics array, regardless of type
// It doesn't filter out invalid types - it processes them as-is
return statistics, false
}
// getSuffixForStat returns the appropriate suffix for annotation names
@@ -281,7 +295,20 @@ func getSuffixForStat(stat interface{}) string {
}
return statString
}
return ""
// For non-string types, convert to string representation like JavaScript does
switch v := stat.(type) {
case map[string]interface{}:
return "[object Object]" // JavaScript behavior for objects
case []interface{}:
return "" // JavaScript behavior for arrays (empty string)
case bool:
if v {
return "true"
}
return "false"
default:
return fmt.Sprintf("%v", stat) // Numbers and other types
}
}
// copyMap creates a shallow copy of a map
@@ -293,12 +320,6 @@ func copyMap(original map[string]interface{}) map[string]interface{} {
return copy
}
// isString checks if value is a string
func isString(value interface{}) bool {
_, ok := value.(string)
return ok
}
// isCloudWatchQuery checks if a query target is a CloudWatch query.
func isCloudWatchQuery(target map[string]interface{}) bool {
// Check for required CloudWatch query fields