Dashboard Migrations: v25 & v24; migrate angular table (#108826)
* Dashboard Migrations: V31 LabelsToFields-Merge Migration * Dashboard Migrations: V32 No-op migration * simplify * Refactor to reduce nesting * Dashboard Migrations: V30 value mappings and tooltip options * Do not automigrate since graph is migrated in v27 * Refactor to reduce nesting * Add test case for invalid mapping * migrate to v29 * wip * Fix tests * fix output * wip * fix min version issue * fix wire * ignore gauge logic as it never get's executed * add panel migration to test * improvements * update * cleanup * address mappings inconsistencies * cleanup * fix lint issues * add cfg when initializing * v27 migration * migrate to v26 * preallocate array * remove logic for grafana-singlestat because it's shared with stat logic; improve error handling and testing * fix go lint * don't preallocate; cleanup comments * cleanup * wip * run internal provider function when getting a single panel * clean up; add tests * add tests for panel plugin service * remove obsolete mock for getting panel plugin * add tests for the whole pipeline * fix test and lint * fix test * Fix missing scenarios --------- Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>
This commit is contained in:
co-authored by
Ivan Ortega
parent
3920b25aee
commit
6ca8c6c6da
@@ -2153,6 +2153,9 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "26"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "27"]
|
||||
],
|
||||
"public/app/features/dashboard/state/DashboardMigratorToBackend.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/dashboard/state/DashboardModel.repeat.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package schemaversion
|
||||
|
||||
var notPersistedProperties = []string{
|
||||
"events",
|
||||
"isViewing",
|
||||
"isEditing",
|
||||
"isInView",
|
||||
"hasRefreshed",
|
||||
"cachedPluginOptions",
|
||||
"plugin",
|
||||
"queryRunner",
|
||||
"replaceVariables",
|
||||
"configRev",
|
||||
"hasSavedPanelEditChange",
|
||||
"getDisplayTitle",
|
||||
"dataSupport",
|
||||
"key",
|
||||
"isNew",
|
||||
"refreshWhenInView",
|
||||
}
|
||||
|
||||
var mustKeepProperties = []string{
|
||||
"id",
|
||||
"gridPos",
|
||||
"type",
|
||||
"title",
|
||||
"scopedVars",
|
||||
"repeat",
|
||||
"repeatPanelId",
|
||||
"repeatDirection",
|
||||
"repeatedByRow",
|
||||
"minSpan",
|
||||
"collapsed",
|
||||
"panels",
|
||||
"targets",
|
||||
"datasource",
|
||||
"timeFrom",
|
||||
"timeShift",
|
||||
"hideTimeOverride",
|
||||
"description",
|
||||
"links",
|
||||
"fullscreen",
|
||||
"isEditing",
|
||||
"isViewing",
|
||||
"hasRefreshed",
|
||||
"events",
|
||||
"cacheTimeout",
|
||||
"queryCachingTTL",
|
||||
"cachedPluginOptions",
|
||||
"transparent",
|
||||
"pluginVersion",
|
||||
"queryRunner",
|
||||
"transformations",
|
||||
"fieldConfig",
|
||||
"maxDataPoints",
|
||||
"interval",
|
||||
"replaceVariables",
|
||||
"libraryPanel",
|
||||
"getDisplayTitle",
|
||||
"configRev",
|
||||
"key",
|
||||
}
|
||||
|
||||
// getOptionsToRemember returns a map of panel properties that should be remembered
|
||||
// during panel type changes, excluding notPersistedProperties and mustKeepProperties
|
||||
func getOptionsToRemember(panel map[string]interface{}) map[string]interface{} {
|
||||
// Create sets for faster lookup
|
||||
notPersistedSet := make(map[string]bool)
|
||||
for _, prop := range notPersistedProperties {
|
||||
notPersistedSet[prop] = true
|
||||
}
|
||||
|
||||
mustKeepSet := make(map[string]bool)
|
||||
for _, prop := range mustKeepProperties {
|
||||
mustKeepSet[prop] = true
|
||||
}
|
||||
|
||||
// Filter the panel properties
|
||||
result := make(map[string]interface{})
|
||||
for key, value := range panel {
|
||||
// Skip properties that are in notPersistedProperties or mustKeepProperties
|
||||
if notPersistedSet[key] || mustKeepSet[key] {
|
||||
continue
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
MIN_VERSION = 25
|
||||
MIN_VERSION = 23
|
||||
LATEST_VERSION = 41
|
||||
)
|
||||
|
||||
@@ -38,6 +38,8 @@ type PanelPluginInfoProvider interface {
|
||||
|
||||
func GetMigrations(dsInfoProvider DataSourceInfoProvider, panelProvider PanelPluginInfoProvider) map[int]SchemaVersionMigrationFunc {
|
||||
return map[int]SchemaVersionMigrationFunc{
|
||||
24: V24(panelProvider),
|
||||
25: V25,
|
||||
26: V26,
|
||||
27: V27,
|
||||
28: V28(panelProvider),
|
||||
|
||||
@@ -0,0 +1,631 @@
|
||||
package schemaversion
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// V24 migration migrates the angular table panel to the standard table panel
|
||||
// In the frontend, this is an auto-migration meaning that this angular panel is always migrated to table panel.
|
||||
// The backend replicates the complete frontend auto-migration logic since it cannot rely on frontend auto-migration.
|
||||
//
|
||||
// This migration performs:
|
||||
// 1. Converts 'styles' array to 'fieldConfig' with 'defaults' and 'overrides'
|
||||
// 2. Migrates thresholds and colors to new threshold format
|
||||
// 3. Converts column-specific styles to field overrides
|
||||
// 4. Migrates transformations from old format to new transformation system
|
||||
// 5. Handles various style properties: unit, decimals, alignment, color modes, links, date formatting, hidden columns
|
||||
// 6. Removes deprecated properties: styles, transform, columns
|
||||
|
||||
// Example 1: Basic table with defaults
|
||||
// Before migration:
|
||||
// {
|
||||
// "panels": [
|
||||
// {
|
||||
// "id": 1,
|
||||
// "type": "table",
|
||||
// "title": "Basic Table",
|
||||
// "styles": [
|
||||
// {
|
||||
// "pattern": "/.*/",
|
||||
// "thresholds": ["10", "20", "30"],
|
||||
// "colors": ["green", "yellow", "red"],
|
||||
// "unit": "bytes",
|
||||
// "decimals": 2
|
||||
// }
|
||||
// ],
|
||||
// "targets": [{ "refId": "A" }]
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// After migration:
|
||||
// {
|
||||
// "panels": [
|
||||
// {
|
||||
// "id": 1,
|
||||
// "type": "table",
|
||||
// "title": "Basic Table",
|
||||
// "fieldConfig": {
|
||||
// "defaults": {
|
||||
// "unit": "bytes",
|
||||
// "decimals": 2,
|
||||
// "custom": {},
|
||||
// "thresholds": {
|
||||
// "mode": "absolute",
|
||||
// "steps": [
|
||||
// { "color": "green", "value": null },
|
||||
// { "color": "green", "value": 10 },
|
||||
// { "color": "yellow", "value": 20 },
|
||||
// { "color": "red", "value": 30 }
|
||||
// ]
|
||||
// }
|
||||
// },
|
||||
// "overrides": []
|
||||
// },
|
||||
// "transformations": [],
|
||||
// "targets": [{ "refId": "A" }],
|
||||
// "pluginVersion": "1.0.0"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
// Example 2: Complex table with overrides and transformations
|
||||
// Before migration:
|
||||
// {
|
||||
// "panels": [
|
||||
// {
|
||||
// "id": 2,
|
||||
// "type": "table",
|
||||
// "title": "Complex Table",
|
||||
// "styles": [
|
||||
// {
|
||||
// "pattern": "/.*/",
|
||||
// "unit": "percent",
|
||||
// "align": "center",
|
||||
// "colorMode": "cell"
|
||||
// },
|
||||
// {
|
||||
// "pattern": "Status",
|
||||
// "alias": "Current Status",
|
||||
// "colorMode": "value",
|
||||
// "align": "left"
|
||||
// },
|
||||
// {
|
||||
// "pattern": "/Error.*/",
|
||||
// "link": true,
|
||||
// "linkUrl": "http://example.com/errors",
|
||||
// "linkTooltip": "View errors",
|
||||
// "linkTargetBlank": true
|
||||
// },
|
||||
// {
|
||||
// "pattern": "Time",
|
||||
// "type": "date",
|
||||
// "dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||
// "alias": "Timestamp"
|
||||
// },
|
||||
// {
|
||||
// "pattern": "Hidden",
|
||||
// "type": "hidden"
|
||||
// }
|
||||
// ],
|
||||
// "transform": "timeseries_aggregations",
|
||||
// "columns": [
|
||||
// { "value": "avg", "text": "Average" },
|
||||
// { "value": "max", "text": "Maximum" }
|
||||
// ],
|
||||
// "targets": [{ "refId": "A" }]
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// After migration:
|
||||
// {
|
||||
// "panels": [
|
||||
// {
|
||||
// "id": 2,
|
||||
// "type": "table",
|
||||
// "title": "Complex Table",
|
||||
// "fieldConfig": {
|
||||
// "defaults": {
|
||||
// "unit": "percent",
|
||||
// "custom": {
|
||||
// "align": "center",
|
||||
// "cellOptions": { "type": "color-background" }
|
||||
// }
|
||||
// },
|
||||
// "overrides": [
|
||||
// {
|
||||
// "matcher": { "id": "byName", "options": "Status" },
|
||||
// "properties": [
|
||||
// { "id": "displayName", "value": "Current Status" },
|
||||
// { "id": "custom.cellOptions", "value": { "type": "color-text" } },
|
||||
// { "id": "custom.align", "value": "left" }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// "matcher": { "id": "byRegexp", "options": "/Error.*/" },
|
||||
// "properties": [
|
||||
// {
|
||||
// "id": "links",
|
||||
// "value": [{
|
||||
// "title": "View errors",
|
||||
// "url": "http://example.com/errors",
|
||||
// "targetBlank": true
|
||||
// }]
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// "matcher": { "id": "byName", "options": "Time" },
|
||||
// "properties": [
|
||||
// { "id": "displayName", "value": "Timestamp" },
|
||||
// { "id": "unit", "value": "time: YYYY-MM-DD HH:mm:ss" }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// "matcher": { "id": "byName", "options": "Hidden" },
|
||||
// "properties": [
|
||||
// { "id": "custom.hidden", "value": true }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// "transformations": [
|
||||
// {
|
||||
// "id": "reduce",
|
||||
// "options": {
|
||||
// "reducers": ["mean", "max"],
|
||||
// "includeTimeField": false
|
||||
// }
|
||||
// }
|
||||
// ],
|
||||
// "targets": [{ "refId": "A" }],
|
||||
// "pluginVersion": "1.0.0"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
type v24Migrator struct {
|
||||
panelProvider PanelPluginInfoProvider
|
||||
panelPlugins []PanelPluginInfo
|
||||
}
|
||||
|
||||
func V24(panelProvider PanelPluginInfoProvider) SchemaVersionMigrationFunc {
|
||||
migrator := &v24Migrator{
|
||||
panelProvider: panelProvider,
|
||||
panelPlugins: panelProvider.GetPanels(),
|
||||
}
|
||||
|
||||
return migrator.migrate
|
||||
}
|
||||
|
||||
func (m *v24Migrator) migrate(dashboard map[string]interface{}) error {
|
||||
dashboard["schemaVersion"] = 24
|
||||
|
||||
panels, ok := dashboard["panels"].([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, panel := range panels {
|
||||
panelMap, ok := panel.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
wasAngularTable := panelMap["type"] == "table"
|
||||
wasReactTable := panelMap["table"] == "table2"
|
||||
|
||||
if wasAngularTable && panelMap["styles"] == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !wasAngularTable || wasReactTable {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find if the panel plugin exists
|
||||
tablePanelPlugin := m.panelProvider.GetPanelPlugin("table")
|
||||
if tablePanelPlugin.ID == "" {
|
||||
return NewMigrationError("table panel plugin not found when migrating dashboard to schema version 24", 24, LATEST_VERSION)
|
||||
}
|
||||
panelMap["pluginVersion"] = tablePanelPlugin.Version
|
||||
err := tablePanelChangedHandler(panelMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func tablePanelChangedHandler(panel map[string]interface{}) error {
|
||||
prevOptions := getOptionsToRemember(panel)
|
||||
|
||||
transformations := migrateTransformations(panel, prevOptions)
|
||||
|
||||
prevDefaults := findDefaultStyle(prevOptions)
|
||||
defaults := migrateDefaults(prevDefaults)
|
||||
|
||||
overrides := findNonDefaultStyles(prevOptions)
|
||||
|
||||
if len(overrides) == 0 {
|
||||
overrides = []interface{}{}
|
||||
}
|
||||
|
||||
panel["transformations"] = transformations
|
||||
panel["fieldConfig"] = map[string]interface{}{
|
||||
"defaults": defaults,
|
||||
"overrides": overrides,
|
||||
}
|
||||
|
||||
// Add default table panel options to match frontend behavior
|
||||
panel["options"] = map[string]interface{}{
|
||||
"cellHeight": "sm",
|
||||
"footer": map[string]interface{}{
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": []interface{}{"sum"},
|
||||
"show": false,
|
||||
},
|
||||
"showHeader": true,
|
||||
}
|
||||
|
||||
// Remove deprecated properties
|
||||
delete(panel, "styles")
|
||||
delete(panel, "transform")
|
||||
delete(panel, "columns")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findDefaultStyle finds the style with pattern '/.*/' (default style)
|
||||
func findDefaultStyle(prevOptions map[string]interface{}) map[string]interface{} {
|
||||
if styles, ok := prevOptions["styles"].([]interface{}); ok {
|
||||
for _, style := range styles {
|
||||
if styleMap, ok := style.(map[string]interface{}); ok {
|
||||
if pattern, ok := styleMap["pattern"].(string); ok && pattern == "/.*/" {
|
||||
return styleMap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findNonDefaultStyles finds all styles that don't have pattern '/.*/'
|
||||
func findNonDefaultStyles(prevOptions map[string]interface{}) []interface{} {
|
||||
var overrides []interface{}
|
||||
|
||||
if styles, ok := prevOptions["styles"].([]interface{}); ok {
|
||||
for _, style := range styles {
|
||||
if styleMap, ok := style.(map[string]interface{}); ok {
|
||||
if pattern, ok := styleMap["pattern"].(string); ok && pattern != "/.*/" {
|
||||
override := migrateTableStyleToOverride(styleMap)
|
||||
overrides = append(overrides, override)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return overrides
|
||||
}
|
||||
|
||||
// migrateTransformations converts old table transformations to new format
|
||||
func migrateTransformations(panel map[string]interface{}, oldOpts map[string]interface{}) []interface{} {
|
||||
transformations := []interface{}{}
|
||||
if existing, ok := panel["transformations"].([]interface{}); ok {
|
||||
transformations = existing
|
||||
}
|
||||
|
||||
// Check if oldOpts has a transform that we can map
|
||||
if transform, ok := oldOpts["transform"].(string); ok {
|
||||
if newTransformID, exists := transformsMap[transform]; exists {
|
||||
opts := map[string]interface{}{
|
||||
"reducers": []interface{}{},
|
||||
}
|
||||
|
||||
// Handle timeseries_aggregations specifically
|
||||
if transform == "timeseries_aggregations" {
|
||||
opts["includeTimeField"] = false
|
||||
|
||||
// Map columns to reducers
|
||||
if columns, ok := oldOpts["columns"].([]interface{}); ok {
|
||||
var reducers []interface{}
|
||||
for _, column := range columns {
|
||||
if columnMap, ok := column.(map[string]interface{}); ok {
|
||||
if value, ok := columnMap["value"].(string); ok {
|
||||
if reducer, exists := columnsMap[value]; exists {
|
||||
reducers = append(reducers, reducer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
opts["reducers"] = reducers
|
||||
}
|
||||
}
|
||||
|
||||
// Add the transformation
|
||||
transformation := map[string]interface{}{
|
||||
"id": newTransformID,
|
||||
"options": opts,
|
||||
}
|
||||
transformations = append(transformations, transformation)
|
||||
}
|
||||
}
|
||||
|
||||
return transformations
|
||||
}
|
||||
|
||||
// transformsMap maps old transform names to new transformation IDs
|
||||
var transformsMap = map[string]string{
|
||||
"timeseries_to_rows": "seriesToRows",
|
||||
"timeseries_to_columns": "seriesToColumns",
|
||||
"timeseries_aggregations": "reduce",
|
||||
"table": "merge",
|
||||
}
|
||||
|
||||
// columnsMap maps old column values to new reducer names
|
||||
var columnsMap = map[string]string{
|
||||
"avg": "mean",
|
||||
"min": "min",
|
||||
"max": "max",
|
||||
"total": "sum",
|
||||
"current": "lastNotNull",
|
||||
"count": "count",
|
||||
}
|
||||
|
||||
// migrateTableStyleToOverride converts a table style to a field config override
|
||||
func migrateTableStyleToOverride(style map[string]interface{}) map[string]interface{} {
|
||||
pattern, _ := style["pattern"].(string)
|
||||
|
||||
// Determine field matcher ID based on pattern
|
||||
fieldMatcherID := "byName"
|
||||
if pattern != "" && len(pattern) >= 2 && pattern[0] == '/' && pattern[len(pattern)-1] == '/' {
|
||||
fieldMatcherID = "byRegexp"
|
||||
}
|
||||
|
||||
override := map[string]interface{}{
|
||||
"matcher": map[string]interface{}{
|
||||
"id": fieldMatcherID,
|
||||
"options": pattern,
|
||||
},
|
||||
"properties": []interface{}{},
|
||||
}
|
||||
|
||||
properties := override["properties"].([]interface{})
|
||||
|
||||
// Add display name
|
||||
if alias, ok := style["alias"].(string); ok && alias != "" {
|
||||
properties = append(properties, map[string]interface{}{
|
||||
"id": "displayName",
|
||||
"value": alias,
|
||||
})
|
||||
}
|
||||
|
||||
// Add unit
|
||||
if unit, ok := style["unit"].(string); ok && unit != "" {
|
||||
properties = append(properties, map[string]interface{}{
|
||||
"id": "unit",
|
||||
"value": unit,
|
||||
})
|
||||
}
|
||||
|
||||
// Add decimals
|
||||
if decimals, ok := style["decimals"].(float64); ok {
|
||||
properties = append(properties, map[string]interface{}{
|
||||
"id": "decimals",
|
||||
"value": int(decimals),
|
||||
})
|
||||
} else if decimals, ok := style["decimals"].(int); ok {
|
||||
properties = append(properties, map[string]interface{}{
|
||||
"id": "decimals",
|
||||
"value": decimals,
|
||||
})
|
||||
}
|
||||
|
||||
// Handle date type
|
||||
if styleType, ok := style["type"].(string); ok && styleType == "date" {
|
||||
if dateFormat, ok := style["dateFormat"].(string); ok {
|
||||
properties = append(properties, map[string]interface{}{
|
||||
"id": "unit",
|
||||
"value": "time: " + dateFormat,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Handle hidden type
|
||||
if styleType, ok := style["type"].(string); ok && styleType == "hidden" {
|
||||
properties = append(properties, map[string]interface{}{
|
||||
"id": "custom.hidden",
|
||||
"value": true,
|
||||
})
|
||||
}
|
||||
|
||||
// Handle links
|
||||
if link, ok := style["link"].(bool); ok && link {
|
||||
linkTooltip, _ := style["linkTooltip"].(string)
|
||||
linkUrl, _ := style["linkUrl"].(string)
|
||||
linkTargetBlank, _ := style["linkTargetBlank"].(bool)
|
||||
|
||||
properties = append(properties, map[string]interface{}{
|
||||
"id": "links",
|
||||
"value": []interface{}{
|
||||
map[string]interface{}{
|
||||
"title": linkTooltip,
|
||||
"url": linkUrl,
|
||||
"targetBlank": linkTargetBlank,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Handle color mode
|
||||
if colorMode, ok := style["colorMode"].(string); ok && colorMode != "" {
|
||||
if newColorMode, exists := colorModeMap[colorMode]; exists {
|
||||
properties = append(properties, map[string]interface{}{
|
||||
"id": "custom.cellOptions",
|
||||
"value": map[string]interface{}{
|
||||
"type": newColorMode,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Handle alignment
|
||||
if align, ok := style["align"].(string); ok && align != "" {
|
||||
alignValue := align
|
||||
if align == "auto" {
|
||||
alignValue = ""
|
||||
}
|
||||
properties = append(properties, map[string]interface{}{
|
||||
"id": "custom.align",
|
||||
"value": alignValue,
|
||||
})
|
||||
}
|
||||
|
||||
// Handle thresholds
|
||||
if thresholds, ok := style["thresholds"].([]interface{}); ok && len(thresholds) > 0 {
|
||||
if colors, ok := style["colors"].([]interface{}); ok && len(colors) > 0 {
|
||||
steps := generateThresholds(thresholds, colors)
|
||||
properties = append(properties, map[string]interface{}{
|
||||
"id": "thresholds",
|
||||
"value": map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": steps,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override["properties"] = properties
|
||||
return override
|
||||
}
|
||||
|
||||
// migrateDefaults converts default table styles to field config defaults
|
||||
func migrateDefaults(prevDefaults map[string]interface{}) map[string]interface{} {
|
||||
defaults := map[string]interface{}{
|
||||
"custom": map[string]interface{}{
|
||||
"align": "auto",
|
||||
"cellOptions": map[string]interface{}{
|
||||
"type": "auto",
|
||||
},
|
||||
"inspect": false,
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
}
|
||||
|
||||
// Only add default thresholds if we have prevDefaults (meaning this is a table panel being migrated)
|
||||
// and no specific thresholds exist in prevDefaults
|
||||
hasThresholds := false
|
||||
if prevDefaults != nil {
|
||||
if thresholds, ok := prevDefaults["thresholds"].([]interface{}); ok && len(thresholds) > 0 {
|
||||
hasThresholds = true
|
||||
}
|
||||
|
||||
// Only add default thresholds for table panels (when prevDefaults exists) without existing thresholds
|
||||
if !hasThresholds {
|
||||
defaults["thresholds"] = map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{"color": "green"},
|
||||
map[string]interface{}{"color": "red", "value": 80},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if prevDefaults == nil {
|
||||
return defaults
|
||||
}
|
||||
|
||||
if unit, ok := prevDefaults["unit"].(string); ok && unit != "" {
|
||||
defaults["unit"] = unit
|
||||
}
|
||||
|
||||
if decimals, ok := prevDefaults["decimals"].(float64); ok {
|
||||
defaults["decimals"] = int(decimals)
|
||||
}
|
||||
|
||||
if alias, ok := prevDefaults["alias"].(string); ok && alias != "" {
|
||||
defaults["displayName"] = alias
|
||||
}
|
||||
|
||||
if align, ok := prevDefaults["align"].(string); ok && align != "" {
|
||||
alignValue := align
|
||||
if align == "auto" {
|
||||
alignValue = ""
|
||||
}
|
||||
defaults["custom"].(map[string]interface{})["align"] = alignValue
|
||||
}
|
||||
|
||||
if thresholds, ok := prevDefaults["thresholds"].([]interface{}); ok && len(thresholds) > 0 {
|
||||
if colors, ok := prevDefaults["colors"].([]interface{}); ok && len(colors) > 0 {
|
||||
steps := generateThresholds(thresholds, colors)
|
||||
defaults["thresholds"] = map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": steps,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if colorMode, ok := prevDefaults["colorMode"].(string); ok && colorMode != "" {
|
||||
if newColorMode, exists := colorModeMap[colorMode]; exists {
|
||||
defaults["custom"].(map[string]interface{})["cellOptions"] = map[string]interface{}{
|
||||
"type": newColorMode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return defaults
|
||||
}
|
||||
|
||||
func generateThresholds(thresholds []interface{}, colors []interface{}) []interface{} {
|
||||
steps := []interface{}{}
|
||||
|
||||
// Add the base step (equivalent to -Infinity)
|
||||
var baseColor interface{} = "red" // default fallback
|
||||
if len(colors) > 0 && colors[0] != nil {
|
||||
baseColor = colors[0]
|
||||
}
|
||||
|
||||
steps = append(steps, map[string]interface{}{
|
||||
"color": baseColor,
|
||||
"value": nil,
|
||||
})
|
||||
|
||||
// Add threshold steps
|
||||
for i, threshold := range thresholds {
|
||||
var color interface{}
|
||||
// Use colors[i+1] for the i-th threshold (colors[0] was used for base step)
|
||||
if i+1 < len(colors) && colors[i+1] != nil {
|
||||
color = colors[i+1]
|
||||
} else {
|
||||
color = "red"
|
||||
}
|
||||
|
||||
var value float64
|
||||
switch v := threshold.(type) {
|
||||
case string:
|
||||
if parsed, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
value = parsed
|
||||
}
|
||||
case float64:
|
||||
value = v
|
||||
case int:
|
||||
value = float64(v)
|
||||
}
|
||||
|
||||
steps = append(steps, map[string]interface{}{
|
||||
"color": color,
|
||||
"value": value,
|
||||
})
|
||||
}
|
||||
|
||||
return steps
|
||||
}
|
||||
|
||||
var colorModeMap = map[string]string{
|
||||
"cell": "color-background",
|
||||
"row": "color-background",
|
||||
"value": "color-text",
|
||||
}
|
||||
@@ -0,0 +1,823 @@
|
||||
package schemaversion_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil"
|
||||
)
|
||||
|
||||
func TestV24(t *testing.T) {
|
||||
tests := []migrationTestCase{
|
||||
{
|
||||
name: "should migrate basic Angular table with defaults",
|
||||
input: map[string]interface{}{
|
||||
"schemaVersion": 23,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"type": "table",
|
||||
"title": "Basic Table",
|
||||
"legend": true,
|
||||
"styles": []interface{}{
|
||||
map[string]interface{}{
|
||||
"thresholds": []interface{}{"10", "20", "30"},
|
||||
"colors": []interface{}{"red", "yellow", "green"},
|
||||
"pattern": "/.*/",
|
||||
},
|
||||
},
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"schemaVersion": 24,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"type": "table",
|
||||
"title": "Basic Table",
|
||||
"legend": true,
|
||||
"fieldConfig": map[string]interface{}{
|
||||
"defaults": map[string]interface{}{
|
||||
"custom": map[string]interface{}{
|
||||
"align": "auto",
|
||||
"cellOptions": map[string]interface{}{
|
||||
"type": "auto",
|
||||
},
|
||||
"inspect": false,
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
"thresholds": map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{"value": nil, "color": "red"},
|
||||
map[string]interface{}{"value": float64(10), "color": "yellow"},
|
||||
map[string]interface{}{"value": float64(20), "color": "green"},
|
||||
map[string]interface{}{"value": float64(30), "color": "red"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
"options": map[string]interface{}{
|
||||
"cellHeight": "sm",
|
||||
"footer": map[string]interface{}{
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": []interface{}{"sum"},
|
||||
"show": false,
|
||||
},
|
||||
"showHeader": true,
|
||||
},
|
||||
"transformations": []interface{}{},
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should migrate table with complex defaults and overrides",
|
||||
input: map[string]interface{}{
|
||||
"schemaVersion": 23,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 2,
|
||||
"type": "table",
|
||||
"title": "Complex Table",
|
||||
"styles": []interface{}{
|
||||
// Default style
|
||||
map[string]interface{}{
|
||||
"pattern": "/.*/",
|
||||
"unit": "bytes",
|
||||
"decimals": float64(2),
|
||||
"align": "center",
|
||||
"colorMode": "cell",
|
||||
"thresholds": []interface{}{"100", "500"},
|
||||
"colors": []interface{}{"green", "yellow", "red"},
|
||||
},
|
||||
// Column-specific override with exact name
|
||||
map[string]interface{}{
|
||||
"pattern": "Status",
|
||||
"alias": "Current Status",
|
||||
"unit": "short",
|
||||
"decimals": float64(0),
|
||||
"colorMode": "value",
|
||||
"align": "left",
|
||||
},
|
||||
// Column-specific override with regex pattern
|
||||
map[string]interface{}{
|
||||
"pattern": "/Error.*/",
|
||||
"link": true,
|
||||
"linkUrl": "http://example.com/errors",
|
||||
"linkTooltip": "View error details",
|
||||
"linkTargetBlank": true,
|
||||
"colorMode": "row",
|
||||
"colors": []interface{}{"red", "orange"},
|
||||
},
|
||||
// Date column
|
||||
map[string]interface{}{
|
||||
"pattern": "Time",
|
||||
"type": "date",
|
||||
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||
"alias": "Timestamp",
|
||||
},
|
||||
// Hidden column
|
||||
map[string]interface{}{
|
||||
"pattern": "Hidden",
|
||||
"type": "hidden",
|
||||
},
|
||||
},
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"schemaVersion": 24,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 2,
|
||||
"type": "table",
|
||||
"title": "Complex Table",
|
||||
"fieldConfig": map[string]interface{}{
|
||||
"defaults": map[string]interface{}{
|
||||
"unit": "bytes",
|
||||
"decimals": 2,
|
||||
"custom": map[string]interface{}{
|
||||
"align": "center",
|
||||
"cellOptions": map[string]interface{}{
|
||||
"type": "color-background",
|
||||
},
|
||||
"inspect": false,
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
"thresholds": map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{"value": nil, "color": "green"},
|
||||
map[string]interface{}{"value": float64(100), "color": "yellow"},
|
||||
map[string]interface{}{"value": float64(500), "color": "red"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"overrides": []interface{}{
|
||||
map[string]interface{}{
|
||||
"matcher": map[string]interface{}{
|
||||
"id": "byName",
|
||||
"options": "Status",
|
||||
},
|
||||
"properties": []interface{}{
|
||||
map[string]interface{}{"id": "displayName", "value": "Current Status"},
|
||||
map[string]interface{}{"id": "unit", "value": "short"},
|
||||
map[string]interface{}{"id": "decimals", "value": 0},
|
||||
map[string]interface{}{"id": "custom.cellOptions", "value": map[string]interface{}{"type": "color-text"}},
|
||||
map[string]interface{}{"id": "custom.align", "value": "left"},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"matcher": map[string]interface{}{
|
||||
"id": "byRegexp",
|
||||
"options": "/Error.*/",
|
||||
},
|
||||
"properties": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": "links",
|
||||
"value": []interface{}{
|
||||
map[string]interface{}{
|
||||
"title": "View error details",
|
||||
"url": "http://example.com/errors",
|
||||
"targetBlank": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{"id": "custom.cellOptions", "value": map[string]interface{}{"type": "color-background"}},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"matcher": map[string]interface{}{
|
||||
"id": "byName",
|
||||
"options": "Time",
|
||||
},
|
||||
"properties": []interface{}{
|
||||
map[string]interface{}{"id": "displayName", "value": "Timestamp"},
|
||||
map[string]interface{}{"id": "unit", "value": "time: YYYY-MM-DD HH:mm:ss"},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"matcher": map[string]interface{}{
|
||||
"id": "byName",
|
||||
"options": "Hidden",
|
||||
},
|
||||
"properties": []interface{}{
|
||||
map[string]interface{}{"id": "custom.hidden", "value": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"options": map[string]interface{}{
|
||||
"cellHeight": "sm",
|
||||
"footer": map[string]interface{}{
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": []interface{}{"sum"},
|
||||
"show": false,
|
||||
},
|
||||
"showHeader": true,
|
||||
},
|
||||
"transformations": []interface{}{},
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should migrate table with timeseries_aggregations transform",
|
||||
input: map[string]interface{}{
|
||||
"schemaVersion": 23,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 3,
|
||||
"type": "table",
|
||||
"title": "Table with Aggregations",
|
||||
"styles": []interface{}{
|
||||
map[string]interface{}{
|
||||
"pattern": "/.*/",
|
||||
"unit": "percent",
|
||||
"decimals": float64(1),
|
||||
},
|
||||
},
|
||||
"transform": "timeseries_aggregations",
|
||||
"columns": []interface{}{
|
||||
map[string]interface{}{"value": "avg", "text": "Average"},
|
||||
map[string]interface{}{"value": "max", "text": "Maximum"},
|
||||
map[string]interface{}{"value": "min", "text": "Minimum"},
|
||||
map[string]interface{}{"value": "total", "text": "Total"},
|
||||
map[string]interface{}{"value": "current", "text": "Current"},
|
||||
map[string]interface{}{"value": "count", "text": "Count"},
|
||||
},
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"schemaVersion": 24,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 3,
|
||||
"type": "table",
|
||||
"title": "Table with Aggregations",
|
||||
"fieldConfig": map[string]interface{}{
|
||||
"defaults": map[string]interface{}{
|
||||
"unit": "percent",
|
||||
"decimals": 1,
|
||||
"custom": map[string]interface{}{
|
||||
"align": "auto",
|
||||
"cellOptions": map[string]interface{}{
|
||||
"type": "auto",
|
||||
},
|
||||
"inspect": false,
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
"thresholds": map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{"color": "green"},
|
||||
map[string]interface{}{"color": "red", "value": 80},
|
||||
},
|
||||
},
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
"options": map[string]interface{}{
|
||||
"cellHeight": "sm",
|
||||
"footer": map[string]interface{}{
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": []interface{}{"sum"},
|
||||
"show": false,
|
||||
},
|
||||
"showHeader": true,
|
||||
},
|
||||
"transformations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": "reduce",
|
||||
"options": map[string]interface{}{
|
||||
"reducers": []interface{}{"mean", "max", "min", "sum", "lastNotNull", "count"},
|
||||
"includeTimeField": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should migrate table with timeseries_to_rows transform",
|
||||
input: map[string]interface{}{
|
||||
"schemaVersion": 23,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 4,
|
||||
"type": "table",
|
||||
"title": "Table with Rows Transform",
|
||||
"styles": []interface{}{
|
||||
map[string]interface{}{
|
||||
"pattern": "/.*/",
|
||||
"unit": "short",
|
||||
},
|
||||
},
|
||||
"transform": "timeseries_to_rows",
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"schemaVersion": 24,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 4,
|
||||
"type": "table",
|
||||
"title": "Table with Rows Transform",
|
||||
"fieldConfig": map[string]interface{}{
|
||||
"defaults": map[string]interface{}{
|
||||
"unit": "short",
|
||||
"custom": map[string]interface{}{
|
||||
"align": "auto",
|
||||
"cellOptions": map[string]interface{}{
|
||||
"type": "auto",
|
||||
},
|
||||
"inspect": false,
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
"thresholds": map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{"color": "green"},
|
||||
map[string]interface{}{"color": "red", "value": 80},
|
||||
},
|
||||
},
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
"options": map[string]interface{}{
|
||||
"cellHeight": "sm",
|
||||
"footer": map[string]interface{}{
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": []interface{}{"sum"},
|
||||
"show": false,
|
||||
},
|
||||
"showHeader": true,
|
||||
},
|
||||
"transformations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": "seriesToRows",
|
||||
"options": map[string]interface{}{
|
||||
"reducers": []interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should migrate table with timeseries_to_columns transform",
|
||||
input: map[string]interface{}{
|
||||
"schemaVersion": 23,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 5,
|
||||
"type": "table",
|
||||
"title": "Table with Columns Transform",
|
||||
"styles": []interface{}{
|
||||
map[string]interface{}{
|
||||
"pattern": "/.*/",
|
||||
"unit": "bytes",
|
||||
},
|
||||
},
|
||||
"transform": "timeseries_to_columns",
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"schemaVersion": 24,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 5,
|
||||
"type": "table",
|
||||
"title": "Table with Columns Transform",
|
||||
"fieldConfig": map[string]interface{}{
|
||||
"defaults": map[string]interface{}{
|
||||
"unit": "bytes",
|
||||
"custom": map[string]interface{}{
|
||||
"align": "auto",
|
||||
"cellOptions": map[string]interface{}{
|
||||
"type": "auto",
|
||||
},
|
||||
"inspect": false,
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
"thresholds": map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{"color": "green"},
|
||||
map[string]interface{}{"color": "red", "value": 80},
|
||||
},
|
||||
},
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
"options": map[string]interface{}{
|
||||
"cellHeight": "sm",
|
||||
"footer": map[string]interface{}{
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": []interface{}{"sum"},
|
||||
"show": false,
|
||||
},
|
||||
"showHeader": true,
|
||||
},
|
||||
"transformations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": "seriesToColumns",
|
||||
"options": map[string]interface{}{
|
||||
"reducers": []interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should migrate table with table merge transform",
|
||||
input: map[string]interface{}{
|
||||
"schemaVersion": 23,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 6,
|
||||
"type": "table",
|
||||
"title": "Table with Merge Transform",
|
||||
"styles": []interface{}{
|
||||
map[string]interface{}{
|
||||
"pattern": "/.*/",
|
||||
"align": "auto",
|
||||
},
|
||||
},
|
||||
"transform": "table",
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"schemaVersion": 24,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 6,
|
||||
"type": "table",
|
||||
"title": "Table with Merge Transform",
|
||||
"fieldConfig": map[string]interface{}{
|
||||
"defaults": map[string]interface{}{
|
||||
"custom": map[string]interface{}{
|
||||
"align": "",
|
||||
"cellOptions": map[string]interface{}{
|
||||
"type": "auto",
|
||||
},
|
||||
"inspect": false,
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
"thresholds": map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{"color": "green"},
|
||||
map[string]interface{}{"color": "red", "value": 80},
|
||||
},
|
||||
},
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
"options": map[string]interface{}{
|
||||
"cellHeight": "sm",
|
||||
"footer": map[string]interface{}{
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": []interface{}{"sum"},
|
||||
"show": false,
|
||||
},
|
||||
"showHeader": true,
|
||||
},
|
||||
"transformations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": "merge",
|
||||
"options": map[string]interface{}{
|
||||
"reducers": []interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should migrate table with existing transformations",
|
||||
input: map[string]interface{}{
|
||||
"schemaVersion": 23,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 7,
|
||||
"type": "table",
|
||||
"title": "Table with Existing Transformations",
|
||||
"styles": []interface{}{
|
||||
map[string]interface{}{
|
||||
"pattern": "/.*/",
|
||||
"unit": "short",
|
||||
},
|
||||
},
|
||||
"transformations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": "filterFieldsByName",
|
||||
"options": map[string]interface{}{
|
||||
"include": map[string]interface{}{
|
||||
"names": []interface{}{"field1", "field2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"transform": "timeseries_to_rows",
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"schemaVersion": 24,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 7,
|
||||
"type": "table",
|
||||
"title": "Table with Existing Transformations",
|
||||
"fieldConfig": map[string]interface{}{
|
||||
"defaults": map[string]interface{}{
|
||||
"unit": "short",
|
||||
"custom": map[string]interface{}{
|
||||
"align": "auto",
|
||||
"cellOptions": map[string]interface{}{
|
||||
"type": "auto",
|
||||
},
|
||||
"inspect": false,
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
"thresholds": map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{"color": "green"},
|
||||
map[string]interface{}{"color": "red", "value": 80},
|
||||
},
|
||||
},
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
"options": map[string]interface{}{
|
||||
"cellHeight": "sm",
|
||||
"footer": map[string]interface{}{
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": []interface{}{"sum"},
|
||||
"show": false,
|
||||
},
|
||||
"showHeader": true,
|
||||
},
|
||||
"transformations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": "filterFieldsByName",
|
||||
"options": map[string]interface{}{
|
||||
"include": map[string]interface{}{
|
||||
"names": []interface{}{"field1", "field2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": "seriesToRows",
|
||||
"options": map[string]interface{}{
|
||||
"reducers": []interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should not migrate angular table without styles",
|
||||
input: map[string]interface{}{
|
||||
"schemaVersion": 23,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 8,
|
||||
"type": "table",
|
||||
"title": "Table without styles",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"schemaVersion": 24,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 8,
|
||||
"type": "table",
|
||||
"title": "Table without styles",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should not migrate react table (table2)",
|
||||
input: map[string]interface{}{
|
||||
"schemaVersion": 23,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 9,
|
||||
"type": "table",
|
||||
"table": "table2",
|
||||
"title": "React table",
|
||||
"styles": []interface{}{
|
||||
map[string]interface{}{
|
||||
"pattern": "/.*/",
|
||||
"unit": "short",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"schemaVersion": 24,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 9,
|
||||
"type": "table",
|
||||
"table": "table2",
|
||||
"title": "React table",
|
||||
"styles": []interface{}{
|
||||
map[string]interface{}{
|
||||
"pattern": "/.*/",
|
||||
"unit": "short",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should not migrate non-table panels",
|
||||
input: map[string]interface{}{
|
||||
"schemaVersion": 23,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 10,
|
||||
"type": "graph",
|
||||
"title": "Graph panel",
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": 11,
|
||||
"type": "singlestat",
|
||||
"title": "Singlestat panel",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"schemaVersion": 24,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 10,
|
||||
"type": "graph",
|
||||
"title": "Graph panel",
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": 11,
|
||||
"type": "singlestat",
|
||||
"title": "Singlestat panel",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should handle mixed numeric and string thresholds",
|
||||
input: map[string]interface{}{
|
||||
"schemaVersion": 23,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 12,
|
||||
"type": "table",
|
||||
"title": "Mixed threshold types",
|
||||
"styles": []interface{}{
|
||||
map[string]interface{}{
|
||||
"pattern": "/.*/",
|
||||
"thresholds": []interface{}{10, "20", 30.5},
|
||||
"colors": []interface{}{"green", "yellow", "orange", "red"},
|
||||
},
|
||||
},
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"schemaVersion": 24,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 12,
|
||||
"type": "table",
|
||||
"title": "Mixed threshold types",
|
||||
"fieldConfig": map[string]interface{}{
|
||||
"defaults": map[string]interface{}{
|
||||
"custom": map[string]interface{}{
|
||||
"align": "auto",
|
||||
"cellOptions": map[string]interface{}{
|
||||
"type": "auto",
|
||||
},
|
||||
"inspect": false,
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
"thresholds": map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{"value": nil, "color": "green"},
|
||||
map[string]interface{}{"value": float64(10), "color": "yellow"},
|
||||
map[string]interface{}{"value": float64(20), "color": "orange"},
|
||||
map[string]interface{}{"value": float64(30.5), "color": "red"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
"options": map[string]interface{}{
|
||||
"cellHeight": "sm",
|
||||
"footer": map[string]interface{}{
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": []interface{}{"sum"},
|
||||
"show": false,
|
||||
},
|
||||
"showHeader": true,
|
||||
},
|
||||
"transformations": []interface{}{},
|
||||
"targets": []interface{}{
|
||||
map[string]interface{}{"refId": "A"},
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runMigrationTests(t, tests, schemaversion.V24(testutil.GetTestPanelProvider()))
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package schemaversion
|
||||
|
||||
// V25 migration is a no-op migration
|
||||
// It only updates the schema version to 25
|
||||
// It's created to keep the migration history consistent with frontend migrator
|
||||
// Variable tag removal is handled in v28 migration
|
||||
|
||||
// Example before migration:
|
||||
// {
|
||||
// "templating": {
|
||||
// "list": [
|
||||
// {
|
||||
// "name": "tags should not be removed",
|
||||
// "type": "query",
|
||||
// "datasource": "prometheus",
|
||||
// "tags": ["tags should not be removed"],
|
||||
// "tagsQuery": "tag should not be removed",
|
||||
// "tagValuesQuery": "tag should not be removed",
|
||||
// "useTags": true,
|
||||
// "options": []
|
||||
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
|
||||
// Example after migration:
|
||||
// {
|
||||
// "templating": {
|
||||
// "list": [
|
||||
// {
|
||||
// "name": "tags should not be removed",
|
||||
// "type": "query",
|
||||
// "datasource": "prometheus",
|
||||
// "tags": ["tags should not be removed"],
|
||||
// "tagsQuery": "tag should not be removed",
|
||||
// "tagValuesQuery": "tag should not be removed",
|
||||
// "useTags": true,
|
||||
// "options": []
|
||||
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
|
||||
func V25(dashboard map[string]interface{}) error {
|
||||
dashboard["schemaVersion"] = int(25)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package schemaversion_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
)
|
||||
|
||||
func TestV25(t *testing.T) {
|
||||
tests := []migrationTestCase{
|
||||
{
|
||||
name: "v25 no-op migration, updates schema version only",
|
||||
input: map[string]interface{}{
|
||||
"title": "V25 No-Op Migration Test Dashboard",
|
||||
"schemaVersion": 24,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "timeseries",
|
||||
"title": "Panel remains unchanged",
|
||||
"id": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V25 No-Op Migration Test Dashboard",
|
||||
"schemaVersion": 25,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "timeseries",
|
||||
"title": "Panel remains unchanged",
|
||||
"id": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
runMigrationTests(t, tests, schemaversion.V25)
|
||||
}
|
||||
@@ -258,6 +258,24 @@ func (m *v28Migrator) migratetSinglestat(panel map[string]interface{}, defaults
|
||||
// Migrate thresholds FIRST (consolidated: both panel types create DEFAULT_THRESHOLDS for empty strings)
|
||||
m.migrateThresholds(angularOpts, defaults)
|
||||
|
||||
// If no thresholds were set from angular migration, add default stat panel thresholds
|
||||
// This matches the behavior of frontend pluginLoaded which adds default thresholds
|
||||
if _, hasThresholds := defaults["thresholds"]; !hasThresholds {
|
||||
defaults["thresholds"] = map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{
|
||||
"color": "green",
|
||||
"value": nil,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"color": "red",
|
||||
"value": 80,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Apply common angular option migrations (value mappings can now use threshold colors)
|
||||
m.applyCommonAngularMigration(panel, defaults, options, angularOpts)
|
||||
|
||||
@@ -298,6 +316,24 @@ func (m *v28Migrator) migrateGrafanaSinglestatPanel(panel map[string]interface{}
|
||||
// Migrate thresholds FIRST (consolidated: both panel types create DEFAULT_THRESHOLDS for empty strings)
|
||||
m.migrateThresholds(angularOpts, defaults)
|
||||
|
||||
// If no thresholds were set from angular migration, add default stat panel thresholds
|
||||
// This matches the behavior of frontend pluginLoaded which adds default thresholds
|
||||
if _, hasThresholds := defaults["thresholds"]; !hasThresholds {
|
||||
defaults["thresholds"] = map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{
|
||||
"color": "green",
|
||||
"value": nil,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"color": "red",
|
||||
"value": 80,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Apply common angular option migrations (value mappings can now use threshold colors)
|
||||
m.applyCommonAngularMigration(panel, defaults, options, angularOpts)
|
||||
|
||||
|
||||
@@ -160,6 +160,19 @@ func TestV28(t *testing.T) {
|
||||
"fixedColor": "#ff0000",
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
"thresholds": map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{
|
||||
"color": "green",
|
||||
"value": nil,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"color": "red",
|
||||
"value": 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
@@ -228,6 +241,19 @@ func TestV28(t *testing.T) {
|
||||
"fixedColor": "#ff0000",
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
"thresholds": map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{
|
||||
"color": "green",
|
||||
"value": nil,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"color": "red",
|
||||
"value": 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
},
|
||||
@@ -512,7 +538,20 @@ func TestV28(t *testing.T) {
|
||||
},
|
||||
"fieldConfig": map[string]interface{}{
|
||||
"defaults": map[string]interface{}{
|
||||
"unit": "short",
|
||||
"unit": "short",
|
||||
"thresholds": map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{
|
||||
"color": "green",
|
||||
"value": nil,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"color": "red",
|
||||
"value": 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
@@ -564,7 +603,20 @@ func TestV28(t *testing.T) {
|
||||
},
|
||||
"fieldConfig": map[string]interface{}{
|
||||
"defaults": map[string]interface{}{
|
||||
"unit": "short",
|
||||
"unit": "short",
|
||||
"thresholds": map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{
|
||||
"color": "green",
|
||||
"value": nil,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"color": "red",
|
||||
"value": 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
@@ -626,7 +678,20 @@ func TestV28(t *testing.T) {
|
||||
},
|
||||
"fieldConfig": map[string]interface{}{
|
||||
"defaults": map[string]interface{}{
|
||||
"unit": "bytes",
|
||||
"unit": "bytes",
|
||||
"thresholds": map[string]interface{}{
|
||||
"mode": "absolute",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{
|
||||
"color": "green",
|
||||
"value": nil,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"color": "red",
|
||||
"value": 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
},
|
||||
"overrides": []interface{}{},
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
{
|
||||
"schemaVersion": 23,
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "table",
|
||||
"title": "Basic Angular Table with Defaults",
|
||||
"description": "Tests basic migration with default style pattern (/.*/) containing thresholds and colors. Should convert styles to fieldConfig.defaults with threshold steps.",
|
||||
"legend": true,
|
||||
"styles": [
|
||||
{
|
||||
"thresholds": ["10", "20", "30"],
|
||||
"colors": ["red", "yellow", "green"],
|
||||
"pattern": "/.*/"
|
||||
}
|
||||
],
|
||||
"targets": [{ "refId": "A" }, {}]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "table",
|
||||
"title": "Complex Table with All Style Features",
|
||||
"description": "Tests comprehensive migration including: default style with thresholds/colors/unit/decimals/align/colorMode, column overrides with exact name and regex patterns, date formatting, hidden columns, and links with tooltips.",
|
||||
"styles": [
|
||||
{
|
||||
"pattern": "/.*/",
|
||||
"thresholds": ["100", "500"],
|
||||
"colors": ["green", "yellow", "red"],
|
||||
"unit": "bytes",
|
||||
"decimals": 2,
|
||||
"align": "center",
|
||||
"colorMode": "cell"
|
||||
},
|
||||
{
|
||||
"pattern": "Status",
|
||||
"alias": "Current Status",
|
||||
"unit": "short",
|
||||
"decimals": 0,
|
||||
"colorMode": "value",
|
||||
"align": "left"
|
||||
},
|
||||
{
|
||||
"pattern": "/Error.*/",
|
||||
"link": true,
|
||||
"linkUrl": "http://example.com/errors",
|
||||
"linkTooltip": "View error details",
|
||||
"linkTargetBlank": true,
|
||||
"colorMode": "row"
|
||||
},
|
||||
{
|
||||
"pattern": "Time",
|
||||
"type": "date",
|
||||
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||
"alias": "Timestamp"
|
||||
},
|
||||
{
|
||||
"pattern": "Hidden",
|
||||
"type": "hidden"
|
||||
}
|
||||
],
|
||||
"targets": [{ "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "table",
|
||||
"title": "Table with Timeseries Aggregations Transform",
|
||||
"description": "Tests migration of timeseries_aggregations transform to reduce transformation with column mappings (avg->mean, max->max, min->min, total->sum, current->lastNotNull, count->count).",
|
||||
"styles": [
|
||||
{
|
||||
"pattern": "/.*/",
|
||||
"unit": "percent",
|
||||
"decimals": 1
|
||||
}
|
||||
],
|
||||
"transform": "timeseries_aggregations",
|
||||
"columns": [
|
||||
{ "value": "avg", "text": "Average" },
|
||||
{ "value": "max", "text": "Maximum" },
|
||||
{ "value": "min", "text": "Minimum" },
|
||||
{ "value": "total", "text": "Total" },
|
||||
{ "value": "current", "text": "Current" },
|
||||
{ "value": "count", "text": "Count" }
|
||||
],
|
||||
"targets": [{ "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "table",
|
||||
"title": "Table with Timeseries to Rows Transform",
|
||||
"description": "Tests migration of timeseries_to_rows transform to seriesToRows transformation.",
|
||||
"styles": [
|
||||
{
|
||||
"pattern": "/.*/",
|
||||
"unit": "short"
|
||||
}
|
||||
],
|
||||
"transform": "timeseries_to_rows",
|
||||
"targets": [{ "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "table",
|
||||
"title": "Table with Timeseries to Columns Transform",
|
||||
"description": "Tests migration of timeseries_to_columns transform to seriesToColumns transformation.",
|
||||
"styles": [
|
||||
{
|
||||
"pattern": "/.*/",
|
||||
"unit": "bytes"
|
||||
}
|
||||
],
|
||||
"transform": "timeseries_to_columns",
|
||||
"targets": [{ "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "table",
|
||||
"title": "Table with Merge Transform",
|
||||
"description": "Tests migration of table transform to merge transformation. Also tests auto alignment conversion to empty string.",
|
||||
"styles": [
|
||||
{
|
||||
"pattern": "/.*/",
|
||||
"align": "auto"
|
||||
}
|
||||
],
|
||||
"transform": "table",
|
||||
"targets": [{ "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "table",
|
||||
"title": "Table with Existing Transformations",
|
||||
"description": "Tests that existing transformations are preserved and new transformation from old format is appended to the list.",
|
||||
"styles": [
|
||||
{
|
||||
"pattern": "/.*/",
|
||||
"unit": "short"
|
||||
}
|
||||
],
|
||||
"transformations": [
|
||||
{
|
||||
"id": "filterFieldsByName",
|
||||
"options": {
|
||||
"include": {
|
||||
"names": ["field1", "field2"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"transform": "timeseries_to_rows",
|
||||
"targets": [{ "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "table",
|
||||
"title": "Mixed Threshold Types",
|
||||
"description": "Tests handling of mixed numeric and string threshold values (int, string, float) with proper type conversion.",
|
||||
"styles": [
|
||||
{
|
||||
"pattern": "/.*/",
|
||||
"thresholds": [10, "20", 30.5],
|
||||
"colors": ["green", "yellow", "orange", "red"]
|
||||
}
|
||||
],
|
||||
"targets": [{ "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "table",
|
||||
"title": "All Color Modes Test",
|
||||
"description": "Tests all color mode mappings: cell->color-background, row->color-background, value->color-text.",
|
||||
"styles": [
|
||||
{
|
||||
"pattern": "/.*/",
|
||||
"colorMode": "cell"
|
||||
},
|
||||
{
|
||||
"pattern": "CellColumn",
|
||||
"colorMode": "cell"
|
||||
},
|
||||
{
|
||||
"pattern": "RowColumn",
|
||||
"colorMode": "row"
|
||||
},
|
||||
{
|
||||
"pattern": "ValueColumn",
|
||||
"colorMode": "value"
|
||||
}
|
||||
],
|
||||
"targets": [{ "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "table",
|
||||
"title": "All Alignment Options",
|
||||
"description": "Tests all alignment options: left, center, right, and auto (should convert to empty string).",
|
||||
"styles": [
|
||||
{
|
||||
"pattern": "/.*/",
|
||||
"align": "center"
|
||||
},
|
||||
{
|
||||
"pattern": "LeftColumn",
|
||||
"align": "left"
|
||||
},
|
||||
{
|
||||
"pattern": "RightColumn",
|
||||
"align": "right"
|
||||
},
|
||||
{
|
||||
"pattern": "AutoColumn",
|
||||
"align": "auto"
|
||||
}
|
||||
],
|
||||
"targets": [{ "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "table",
|
||||
"title": "Field Matcher Types Test",
|
||||
"description": "Tests both field matcher types: byName for exact matches and byRegexp for regex patterns.",
|
||||
"styles": [
|
||||
{
|
||||
"pattern": "/.*/",
|
||||
"unit": "short"
|
||||
},
|
||||
{
|
||||
"pattern": "ExactColumnName",
|
||||
"alias": "Exact Match"
|
||||
},
|
||||
{
|
||||
"pattern": "/Regex.*Pattern/",
|
||||
"alias": "Regex Match"
|
||||
},
|
||||
{
|
||||
"pattern": "/^Start/",
|
||||
"alias": "Start Pattern"
|
||||
},
|
||||
{
|
||||
"pattern": "/End$/",
|
||||
"alias": "End Pattern"
|
||||
}
|
||||
],
|
||||
"targets": [{ "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "table",
|
||||
"title": "Link Configuration Test",
|
||||
"description": "Tests various link configurations: with and without tooltip, with and without target blank.",
|
||||
"styles": [
|
||||
{
|
||||
"pattern": "/.*/",
|
||||
"unit": "short"
|
||||
},
|
||||
{
|
||||
"pattern": "LinkWithTooltip",
|
||||
"link": true,
|
||||
"linkUrl": "http://example.com/with-tooltip",
|
||||
"linkTooltip": "Click to view details",
|
||||
"linkTargetBlank": true
|
||||
},
|
||||
{
|
||||
"pattern": "LinkWithoutTooltip",
|
||||
"link": true,
|
||||
"linkUrl": "http://example.com/no-tooltip",
|
||||
"linkTargetBlank": false
|
||||
},
|
||||
{
|
||||
"pattern": "LinkMinimal",
|
||||
"link": true,
|
||||
"linkUrl": "http://example.com/minimal"
|
||||
}
|
||||
],
|
||||
"targets": [{ "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"type": "table",
|
||||
"title": "Date Format Variations",
|
||||
"description": "Tests various date format patterns and aliases.",
|
||||
"styles": [
|
||||
{
|
||||
"pattern": "/.*/",
|
||||
"unit": "short"
|
||||
},
|
||||
{
|
||||
"pattern": "DateISO",
|
||||
"type": "date",
|
||||
"dateFormat": "YYYY-MM-DD",
|
||||
"alias": "ISO Date"
|
||||
},
|
||||
{
|
||||
"pattern": "DateTime",
|
||||
"type": "date",
|
||||
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||
"alias": "Full DateTime"
|
||||
},
|
||||
{
|
||||
"pattern": "TimeOnly",
|
||||
"type": "date",
|
||||
"dateFormat": "HH:mm:ss",
|
||||
"alias": "Time Only"
|
||||
}
|
||||
],
|
||||
"targets": [{ "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "table",
|
||||
"title": "React Table - Should NOT Migrate",
|
||||
"description": "React table (table2) should not be migrated. Properties should remain unchanged.",
|
||||
"table": "table2",
|
||||
"styles": [
|
||||
{
|
||||
"pattern": "/.*/",
|
||||
"unit": "short",
|
||||
"thresholds": ["10", "20"],
|
||||
"colors": ["red", "yellow", "green"]
|
||||
}
|
||||
],
|
||||
"targets": [{ "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"type": "table",
|
||||
"title": "Angular Table without Styles - Should NOT Migrate",
|
||||
"description": "Angular table without styles property should not be migrated.",
|
||||
"targets": [{ "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"type": "graph",
|
||||
"title": "Non-Table Panel - Should NOT Migrate",
|
||||
"description": "Non-table panels should remain completely unchanged.",
|
||||
"targets": [{ "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "singlestat",
|
||||
"title": "Singlestat Panel - Should NOT Migrate",
|
||||
"description": "Other panel types should not be affected by table migration.",
|
||||
"targets": [{ "refId": "A" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"title": "V25 No-Op Migration Test Dashboard",
|
||||
"schemaVersion": 24,
|
||||
"panels": [
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "Panel with transformations remains unchanged",
|
||||
"id": 1,
|
||||
"transformations": [
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "graph",
|
||||
"title": "Graph",
|
||||
"id": 2,
|
||||
"yAxes": [
|
||||
{
|
||||
"show": true,
|
||||
"min": null,
|
||||
"max": null
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"name": "tags should not be removed",
|
||||
"type": "query",
|
||||
"datasource": "prometheus",
|
||||
"tags": ["tags should not be removed"],
|
||||
"tagsQuery": "tag should not be removed",
|
||||
"tagValuesQuery": "tag should not be removed",
|
||||
"useTags": true,
|
||||
"options": []
|
||||
|
||||
}
|
||||
]
|
||||
},
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"name": "Deployments",
|
||||
"datasource": "grafana",
|
||||
"enable": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"datasource": {
|
||||
"uid": "grafana"
|
||||
},
|
||||
"enable": true,
|
||||
"name": "Deployments"
|
||||
}
|
||||
]
|
||||
},
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"id": 1,
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Panel with transformations remains unchanged",
|
||||
"transformations": [],
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"id": 2,
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Graph",
|
||||
"type": "graph",
|
||||
"yAxes": [
|
||||
{
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"refresh": "",
|
||||
"schemaVersion": 41,
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"datasource": {
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"name": "tags should not be removed",
|
||||
"options": [],
|
||||
"refresh": 1,
|
||||
"type": "query"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
]
|
||||
},
|
||||
"title": "V25 No-Op Migration Test Dashboard"
|
||||
}
|
||||
@@ -78,6 +78,10 @@ func (m *TestPanelProvider) GetPanels() []schemaversion.PanelPluginInfo {
|
||||
ID: "stat",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
{
|
||||
ID: "table",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
// Note: grafana-singlestat-panel is not included to match frontend test environment
|
||||
// This ensures both frontend and backend migrations produce the same result
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { mockDataSource } from 'app/features/alerting/unified/mocks';
|
||||
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
|
||||
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||
import { plugin as statPanelPlugin } from 'app/plugins/panel/stat/module';
|
||||
import { plugin as tablePanelPlugin } from 'app/plugins/panel/table/module';
|
||||
|
||||
import { DASHBOARD_SCHEMA_VERSION } from './DashboardMigrator';
|
||||
import { DashboardModel } from './DashboardModel';
|
||||
@@ -141,7 +142,7 @@ describe('Backend / Frontend result comparison', () => {
|
||||
which means that the actual migration logic is not run.
|
||||
We need to manually run the pluginLoaded logic to ensure the panels are migrated correctly.
|
||||
*/
|
||||
if (jsonInput.schemaVersion === 27) {
|
||||
if (jsonInput.schemaVersion <= 27) {
|
||||
for (const panel of frontendModel.panels) {
|
||||
if (panel.type === 'stat') {
|
||||
// Set the plugin version if it doesn't exist
|
||||
@@ -165,6 +166,28 @@ describe('Backend / Frontend result comparison', () => {
|
||||
|
||||
await panel.pluginLoaded(statPanelPlugin);
|
||||
}
|
||||
if (panel.type === 'table' && panel.autoMigrateFrom === 'table-old') {
|
||||
// Set the plugin version if it doesn't exist
|
||||
if (!tablePanelPlugin.meta.info) {
|
||||
tablePanelPlugin.meta.info = {
|
||||
author: {
|
||||
name: 'Grafana Labs',
|
||||
url: 'url/to/GrafanaLabs',
|
||||
},
|
||||
description: 'table plugin',
|
||||
links: [{ name: 'project', url: 'one link' }],
|
||||
logos: { small: 'small/logo', large: 'large/logo' },
|
||||
screenshots: [],
|
||||
updated: '2024-01-01',
|
||||
version: '1.0.0',
|
||||
};
|
||||
}
|
||||
if (!tablePanelPlugin.meta.info.version) {
|
||||
tablePanelPlugin.meta.info.version = '1.0.0';
|
||||
}
|
||||
|
||||
await panel.pluginLoaded(tablePanelPlugin as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +199,18 @@ describe('Backend / Frontend result comparison', () => {
|
||||
// Because Go and TS handle -Infinity differently.
|
||||
const cleanedFrontendResult = sortedDeepCloneWithoutNulls(frontendMigrationResult);
|
||||
|
||||
// Remove deprecated angular properties that backend shouldn't return, but DashboardModel will still set them
|
||||
for (const panel of cleanedFrontendResult.panels ?? []) {
|
||||
// @ts-expect-error
|
||||
delete panel.autoMigrateFrom;
|
||||
// @ts-expect-error
|
||||
delete panel.styles;
|
||||
// @ts-expect-error - Backend removes these deprecated table properties
|
||||
delete panel.transform;
|
||||
// @ts-expect-error - Backend removes these deprecated table properties
|
||||
delete panel.columns;
|
||||
}
|
||||
|
||||
expect(backendMigrationResult).toMatchObject(cleanedFrontendResult);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -136,7 +136,7 @@ const migrateTableStyleToOverride = (style: Style) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (style.decimals) {
|
||||
if (style.decimals !== undefined) {
|
||||
override.properties.push({
|
||||
id: 'decimals',
|
||||
value: style.decimals,
|
||||
@@ -186,7 +186,7 @@ const migrateTableStyleToOverride = (style: Style) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (style.thresholds?.length) {
|
||||
if (style.thresholds?.length && style.colors?.length) {
|
||||
override.properties.push({
|
||||
id: 'thresholds',
|
||||
value: {
|
||||
@@ -216,7 +216,7 @@ const migrateDefaults = (prevDefaults: Style) => {
|
||||
isNil
|
||||
);
|
||||
|
||||
if (prevDefaults.thresholds.length) {
|
||||
if (prevDefaults.thresholds && prevDefaults.thresholds.length) {
|
||||
const thresholds: ThresholdsConfig = {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: generateThresholds(prevDefaults.thresholds, prevDefaults.colors),
|
||||
|
||||
Reference in New Issue
Block a user