remove v28

This commit is contained in:
Haris Rozajac 2025-10-08 12:54:52 -06:00
parent cc873d1458
commit 1132c84fa3
20 changed files with 1503 additions and 2395 deletions

View File

@ -549,6 +549,7 @@ func cleanupPanelForSaveWithContext(panel map[string]interface{}, isNested bool)
// Clean up internal markers
delete(panel, "_originallyHadTransformations")
delete(panel, "_originallyHadFieldConfigCustom")
}
// filterDefaultValues removes properties that match the default values (matches frontend's isEqual logic)
@ -600,8 +601,21 @@ func filterDefaultValues(panel map[string]interface{}, originalProperties map[st
for prop, defaultValue := range defaults {
if panelValue, exists := panel[prop]; exists {
if isEqual(panelValue, defaultValue) {
// Special case: fieldConfig is always removed if it matches defaults (frontend getSaveModel behavior)
// Special case: fieldConfig - handle preservation of original custom object first
if prop == "fieldConfig" {
// Check if we need to preserve the custom object before removing fieldConfig
if panel["_originallyHadFieldConfigCustom"] == true {
// Ensure fieldConfig structure exists with custom object
if fieldConfig, ok := panelValue.(map[string]interface{}); ok {
if defaults, ok := fieldConfig["defaults"].(map[string]interface{}); ok {
if _, hasCustom := defaults["custom"]; !hasCustom {
defaults["custom"] = map[string]interface{}{}
}
// Don't remove fieldConfig if we added custom back
continue
}
}
}
delete(panel, prop)
} else {
// Only remove if it wasn't originally present in the input
@ -616,12 +630,32 @@ func filterDefaultValues(panel map[string]interface{}, originalProperties map[st
// Remove empty targets arrays (frontend removes them in cleanup)
removeIfDefaultValue(panel, "targets", []interface{}{})
// Handle case where fieldConfig was removed but originally had custom object
if panel["_originallyHadFieldConfigCustom"] == true {
if _, hasFieldConfig := panel["fieldConfig"]; !hasFieldConfig {
// Recreate fieldConfig with custom object
panel["fieldConfig"] = map[string]interface{}{
"defaults": map[string]interface{}{
"custom": map[string]interface{}{},
},
"overrides": []interface{}{},
}
}
}
// Clean up fieldConfig to match frontend behavior
if fieldConfig, exists := panel["fieldConfig"].(map[string]interface{}); exists {
// Clean up fieldConfig defaults to match frontend behavior
if defaults, hasDefaults := fieldConfig["defaults"].(map[string]interface{}); hasDefaults {
// Remove properties that frontend considers as defaults and omits
cleanupFieldConfigDefaults(defaults, panel)
// Preserve custom object if it was originally present, even if empty
if panel["_originallyHadFieldConfigCustom"] == true {
if _, hasCustom := defaults["custom"]; !hasCustom {
defaults["custom"] = map[string]interface{}{}
}
}
}
}
}
@ -1085,3 +1119,36 @@ func trackPanelOriginalTransformations(panel map[string]interface{}) {
}
}
}
// trackOriginalFieldConfigCustom marks panels that had fieldConfig.defaults.custom in the original input
// This is needed to match frontend hasOwnProperty behavior and preserve empty custom objects
func trackOriginalFieldConfigCustom(dashboard map[string]interface{}) {
if panels, ok := dashboard["panels"].([]interface{}); ok {
for _, panelInterface := range panels {
if panel, ok := panelInterface.(map[string]interface{}); ok {
trackPanelOriginalFieldConfigCustom(panel)
}
}
}
}
// trackPanelOriginalFieldConfigCustom recursively tracks fieldConfig.defaults.custom in panels and nested panels
func trackPanelOriginalFieldConfigCustom(panel map[string]interface{}) {
// Mark if this panel had fieldConfig.defaults.custom in original input
if fieldConfig, ok := panel["fieldConfig"].(map[string]interface{}); ok {
if defaults, ok := fieldConfig["defaults"].(map[string]interface{}); ok {
if _, hasCustom := defaults["custom"]; hasCustom {
panel["_originallyHadFieldConfigCustom"] = true
}
}
}
// Handle nested panels in row panels
if nestedPanels, ok := panel["panels"].([]interface{}); ok {
for _, nestedPanelInterface := range nestedPanels {
if nestedPanel, ok := nestedPanelInterface.(map[string]interface{}); ok {
trackPanelOriginalFieldConfigCustom(nestedPanel)
}
}
}
}

View File

@ -807,6 +807,193 @@ func TestTransformationsArrayContextAwareLogic(t *testing.T) {
}
}
func TestTrackOriginalFieldConfigCustom(t *testing.T) {
tests := []struct {
name string
input map[string]interface{}
expected map[string]interface{}
}{
{
name: "track_top_level_panel_with_fieldConfig_custom",
input: map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "timeseries",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"custom": map[string]interface{}{},
},
"overrides": []interface{}{},
},
},
map[string]interface{}{
"id": 2,
"type": "table",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{},
"overrides": []interface{}{},
},
},
map[string]interface{}{
"id": 3,
"type": "stat",
"title": "Panel without fieldConfig",
},
},
},
expected: map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "timeseries",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"custom": map[string]interface{}{},
},
"overrides": []interface{}{},
},
"_originallyHadFieldConfigCustom": true, // marker added
},
map[string]interface{}{
"id": 2,
"type": "table",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{},
"overrides": []interface{}{},
},
// no marker added - no custom object
},
map[string]interface{}{
"id": 3,
"type": "stat",
"title": "Panel without fieldConfig",
// no marker added - no fieldConfig
},
},
},
},
{
name: "track_nested_panels_in_row_with_fieldConfig_custom",
input: map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "row",
"title": "Row Panel",
"panels": []interface{}{
map[string]interface{}{
"id": 10,
"type": "singlestat",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"custom": map[string]interface{}{},
},
"overrides": []interface{}{},
},
},
map[string]interface{}{
"id": 11,
"type": "graph",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"unit": "bytes",
},
"overrides": []interface{}{},
},
},
},
},
},
},
expected: map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "row",
"title": "Row Panel",
"panels": []interface{}{
map[string]interface{}{
"id": 10,
"type": "singlestat",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"custom": map[string]interface{}{},
},
"overrides": []interface{}{},
},
"_originallyHadFieldConfigCustom": true, // marker added to nested panel
},
map[string]interface{}{
"id": 11,
"type": "graph",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"unit": "bytes",
},
"overrides": []interface{}{},
},
// no marker added - no custom object
},
},
},
},
},
},
{
name: "track_panels_with_non_empty_custom",
input: map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "timeseries",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"custom": map[string]interface{}{
"axisPlacement": "left",
},
},
"overrides": []interface{}{},
},
},
},
},
expected: map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"type": "timeseries",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"custom": map[string]interface{}{
"axisPlacement": "left",
},
},
"overrides": []interface{}{},
},
"_originallyHadFieldConfigCustom": true, // marker added for non-empty custom
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a deep copy of input for testing
dashboard := deepCopy(tt.input).(map[string]interface{})
// Apply the tracking logic
trackOriginalFieldConfigCustom(dashboard)
// Verify the result
if !compareValues(dashboard, tt.expected) {
t.Errorf("Test %s failed.\nExpected: %+v\nGot: %+v", tt.name, tt.expected, dashboard)
}
})
}
}
func TestTrackOriginalTransformations(t *testing.T) {
tests := []struct {
name string
@ -915,6 +1102,123 @@ func TestTrackOriginalTransformations(t *testing.T) {
}
}
func TestCleanupPanelForSavePreservesOriginalFieldConfigCustom(t *testing.T) {
tests := []struct {
name string
input map[string]interface{}
expected map[string]interface{}
}{
{
name: "preserve_empty_custom_when_originally_present",
input: map[string]interface{}{
"type": "singlestat",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"custom": map[string]interface{}{}, // Empty but originally present
},
"overrides": []interface{}{},
},
"_originallyHadFieldConfigCustom": true, // Marker indicating it was originally present
},
expected: map[string]interface{}{
"type": "stat", // Auto-migrated from singlestat
"autoMigrateFrom": "singlestat", // Auto-migration marker
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"custom": map[string]interface{}{}, // Should be preserved
},
"overrides": []interface{}{},
},
},
},
{
name: "remove_empty_custom_when_not_originally_present",
input: map[string]interface{}{
"type": "timeseries",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"custom": map[string]interface{}{}, // Empty and not originally present
},
"overrides": []interface{}{},
},
// No marker - custom was not originally present
},
expected: map[string]interface{}{
"type": "timeseries",
// fieldConfig should be removed entirely as it matches defaults
},
},
{
name: "preserve_non_empty_custom_regardless_of_marker",
input: map[string]interface{}{
"type": "table",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"custom": map[string]interface{}{
"displayMode": "list",
},
},
"overrides": []interface{}{},
},
},
expected: map[string]interface{}{
"type": "table",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"custom": map[string]interface{}{
"displayMode": "list",
},
},
"overrides": []interface{}{},
},
},
},
{
name: "add_custom_when_originally_present_but_missing",
input: map[string]interface{}{
"type": "stat",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{}, // custom missing
"overrides": []interface{}{},
},
"_originallyHadFieldConfigCustom": true, // But marker indicates it was originally present
},
expected: map[string]interface{}{
"type": "stat",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"custom": map[string]interface{}{}, // Should be added back
},
"overrides": []interface{}{},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
panel := make(map[string]interface{})
for k, v := range tt.input {
panel[k] = v
}
cleanupPanelForSaveWithContext(panel, false)
// Verify expected properties exist
for key, expectedValue := range tt.expected {
if actualValue, exists := panel[key]; !exists {
t.Errorf("Property %s should exist but is missing", key)
} else if !compareValues(actualValue, expectedValue) {
t.Errorf("Property %s has wrong value. Expected: %v, Got: %v", key, expectedValue, actualValue)
}
}
// Verify internal markers are cleaned up
assertPropertyRemoved(t, panel, "_originallyHadFieldConfigCustom")
})
}
}
func TestCleanupPanelForSave(t *testing.T) {
tests := []struct {
name string

View File

@ -66,6 +66,10 @@ func (m *migrator) migrate(ctx context.Context, dash map[string]interface{}, tar
// This is needed to match frontend hasOwnProperty behavior
trackOriginalTransformations(dash)
// 1.1. Track which panels had fieldConfig.defaults.custom in original input
// This is needed to preserve empty custom objects that were originally present
trackOriginalFieldConfigCustom(dash)
// 2. Apply ALL frontend defaults FIRST (DashboardModel + PanelModel defaults)
// This replicates the behavior of the frontend DashboardModel and PanelModel constructors
applyFrontendDefaults(dash)

View File

@ -2,61 +2,32 @@ package schemaversion
import (
"context"
"fmt"
"strconv"
"strings"
)
// V28 migrates singlestat panels to stat/gauge panels and removes deprecated variable properties.
//
// The migration performs two main tasks:
// 1. Migrates singlestat panels to either stat or gauge panels based on their configuration
// 2. Removes deprecated variable properties (tags, tagsQuery, tagValuesQuery, useTags)
//
// The migration includes comprehensive logic from the frontend:
// - Panel type migration (singlestat -> stat/gauge)
// - Field config migration with thresholds, mappings, and display options
// - Options migration including reduceOptions, orientation, and other panel-specific settings
// - Support for both angular singlestat and grafana-singlestat-panel migrations
// V28 removes deprecated variable properties (tags, tagsQuery, tagValuesQuery, useTags)
//
// Example before migration:
//
// "panels": [
// {
// "type": "singlestat",
// "gauge": { "show": true },
// "targets": [{ "refId": "A" }]
// }
// ],
// "templating": {
// "list": [
// { "name": "var1", "tags": ["tag1"], "tagsQuery": "query", "tagValuesQuery": "values", "useTags": true }
// ]
// {
// "templating": {
// "list": [
// { "name": "var1", "tags": ["tag1"], "tagsQuery": "query", "tagValuesQuery": "values", "useTags": true }
// ]
// }
// }
//
// Example after migration:
//
// "panels": [
// {
// "type": "gauge",
// "targets": [{ "refId": "A" }]
// }
// ],
// "templating": {
// "list": [
// { "name": "var1" }
// ]
// {
// "templating": {
// "list": [
// { "name": "var1" }
// ]
// }
// }
func V28(_ context.Context, dashboard map[string]interface{}) error {
dashboard["schemaVersion"] = 28
// Migrate singlestat panels
if panels, ok := dashboard["panels"].([]interface{}); ok {
if err := processPanels(panels); err != nil {
return err
}
}
// Remove deprecated variable properties
if templating, ok := dashboard["templating"].(map[string]interface{}); ok {
if list, ok := templating["list"].([]interface{}); ok {
@ -71,751 +42,6 @@ func V28(_ context.Context, dashboard map[string]interface{}) error {
return nil
}
func processPanels(panels []interface{}) error {
for _, panel := range panels {
p, ok := panel.(map[string]interface{})
if !ok {
continue
}
// Process nested panels if this is a row panel
if p["type"] == "row" {
if nestedPanels, ok := p["panels"].([]interface{}); ok {
if err := processPanels(nestedPanels); err != nil {
return err
}
}
continue
}
// Migrate singlestat panels (including those already auto-migrated to stat)
if p["type"] == "singlestat" || p["type"] == "grafana-singlestat-panel" ||
p["autoMigrateFrom"] == "singlestat" || p["autoMigrateFrom"] == "grafana-singlestat-panel" {
if err := migrateSinglestatPanel(p); err != nil {
return err
}
}
// Note: Panel defaults (including options object) are already applied
// by applyPanelDefaults() in the main migration flow for ALL panels
// No need for stat-specific normalization
}
return nil
}
func migrateSinglestatPanel(panel map[string]interface{}) error {
targetType := "stat"
// NOTE: The legacy types "singlestat" and "gauge" are both angular only
// This are not supported by any version that could run this migration, so there is
// no need to maintain a distinction or fallback to the non-stat version
// NOTE: DashboardMigrator's migrateSinglestat function has some logic that never gets called
// migrateSinglestat will only run if (panel.type === 'singlestat')
// but this will not be the case because PanelModel runs restoreModel in the constructor
// and since singlestat is in the autoMigrateAngular map, it will be migrated to stat,
// and therefore migrateSinglestat will never run so this logic inside of it will never apply
// if ((panel as any).gauge?.show) {
// gaugePanelPlugin.meta = config.panels['gauge']
// panel.changePlugin(gaugePanelPlugin)
// Store original type for migration context (only for stat/gauge migration)
// Set autoMigrateFrom to track the original type for proper migration logic
originalType := panel["type"].(string)
// Only set autoMigrateFrom if it doesn't already exist (preserve frontend defaults)
if _, exists := panel["autoMigrateFrom"]; !exists {
panel["autoMigrateFrom"] = originalType
}
panel["type"] = targetType
panel["pluginVersion"] = pluginVersionForAutoMigrate
// Migrate panel options and field config
migrateSinglestatOptions(panel, originalType)
return nil
}
// migrateSinglestatOptions handles the complete migration of singlestat panel options and field config
func migrateSinglestatOptions(panel map[string]interface{}, originalType string) {
// Preserve important panel-level properties that should not be removed
// These properties are preserved by the frontend's getSaveModel() method
var maxDataPoints interface{}
if mdp, exists := panel["maxDataPoints"]; exists {
maxDataPoints = mdp
}
// Initialize field config if not present
if panel["fieldConfig"] == nil {
panel["fieldConfig"] = map[string]interface{}{
"defaults": map[string]interface{}{},
"overrides": []interface{}{},
}
}
fieldConfig := panel["fieldConfig"].(map[string]interface{})
defaults := fieldConfig["defaults"].(map[string]interface{})
// Migrate from angular singlestat configuration using appropriate strategy
// Use autoMigrateFrom if available, otherwise use originalType
migrationType := originalType
if autoMigrateFrom, exists := panel["autoMigrateFrom"].(string); exists {
migrationType = autoMigrateFrom
}
if migrationType == "grafana-singlestat-panel" {
migrateGrafanaSinglestatPanel(panel, defaults)
} else {
migratetSinglestat(panel, defaults)
}
// Apply shared migration logic
applySharedSinglestatMigration(defaults)
// Apply complete stat panel defaults (matches frontend getPanelOptionsWithDefaults)
// The frontend applies these defaults after migration via applyPluginOptionDefaults
applyCompleteStatPanelDefaults(panel)
// Create proper fieldConfig structure from defaults
createFieldConfigFromDefaults(panel, defaults)
// Restore preserved panel-level properties
if maxDataPoints != nil {
panel["maxDataPoints"] = maxDataPoints
}
// Clean up old angular properties after migration
cleanupAngularProperties(panel)
}
// getDefaultStatOptions returns the default options structure for stat panels
// This matches the frontend's stat panel defaultOptions exactly
func getDefaultStatOptions() map[string]interface{} {
// For now, return the explicit defaults until we integrate the centralized system
return map[string]interface{}{
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"percentChangeColorMode": "standard",
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true,
"reduceOptions": map[string]interface{}{
"calcs": []string{"lastNotNull"}, // Matches frontend: ReducerID.lastNotNull
"fields": "",
"values": false,
},
"orientation": "auto",
}
}
// migratetSinglestat handles explicit migration from 'singlestat' panels
// Based on frontend migrateFromAngularSinglestat function
func migratetSinglestat(panel map[string]interface{}, defaults map[string]interface{}) {
angularOpts := extractAngularOptions(panel)
// Extract valueName for reducer mapping (matches frontend migrateFromAngularSinglestat)
var valueName string
if vn, ok := angularOpts["valueName"].(string); ok {
valueName = vn
}
// Set calcs based on valueName (matches frontend: calcs: [reducer ? reducer.id : ReducerID.mean])
var calcs []string
if reducer := getReducerForValueName(valueName); reducer != "" {
calcs = []string{reducer}
} else {
// Use mean as fallback (matches frontend migrateFromAngularSinglestat: ReducerID.mean)
calcs = []string{"mean"}
}
// Create options exactly like frontend migrateFromAngularSinglestat
options := map[string]interface{}{
"reduceOptions": map[string]interface{}{
"calcs": calcs,
"fields": "",
"values": false,
},
"orientation": "horizontal", // Matches frontend migrateFromAngularSinglestat: VizOrientation.Horizontal
}
// Migrate thresholds FIRST (consolidated: both panel types create DEFAULT_THRESHOLDS for empty strings)
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": (*float64)(nil),
},
map[string]interface{}{
"color": "red",
"value": 80,
},
},
}
}
// Apply common angular option migrations (value mappings can now use threshold colors)
applyCommonAngularMigration(panel, defaults, options, angularOpts)
// Merge new options with existing panel options to preserve properties like maxDataPoints
if existingOptions, exists := panel["options"].(map[string]interface{}); exists {
for key, value := range options {
existingOptions[key] = value
}
} else {
panel["options"] = options
}
}
// migrateGrafanaSinglestatPanel handles auto-migration from 'grafana-singlestat-panel'
// Uses the same migration logic as singlestat panels since the frontend applies
// migrateFromAngularSinglestat to both panel types.
func migrateGrafanaSinglestatPanel(panel map[string]interface{}, defaults map[string]interface{}) {
migratetSinglestat(panel, defaults)
}
// migrateThresholds handles threshold migration for both singlestat panel types
// Both panel types now create DEFAULT_THRESHOLDS when threshold string is empty (consolidated behavior)
func migrateThresholds(angularOpts map[string]interface{}, defaults map[string]interface{}) {
if thresholds, ok := angularOpts["thresholds"].(string); ok {
if colors, ok := angularOpts["colors"].([]interface{}); ok {
if thresholds != "" {
// Non-empty thresholds: use normal migration
migrateThresholdsAndColors(defaults, thresholds, colors)
} else {
// Empty thresholds: use frontend DEFAULT_THRESHOLDS fallback (both panel types)
defaults["thresholds"] = map[string]interface{}{
"mode": "absolute",
"steps": []interface{}{
map[string]interface{}{
"color": "green",
"value": (*float64)(nil), // Use pointer to ensure field is present in JSON
},
map[string]interface{}{
"color": "red",
"value": 80,
},
},
}
}
}
}
}
// applyCommonAngularMigration applies migrations common to both singlestat types
func applyCommonAngularMigration(panel map[string]interface{}, defaults map[string]interface{}, options map[string]interface{}, angularOpts map[string]interface{}) {
// Migrate table column
// Based on sharedSingleStatPanelChangedHandler line ~125: options.reduceOptions.fields = `/^${prevPanel.tableColumn}$/`
if tableColumn, ok := angularOpts["tableColumn"].(string); ok && tableColumn != "" {
options["reduceOptions"].(map[string]interface{})["fields"] = "/^" + tableColumn + "$/"
}
// Migrate unit from format property (matches frontend sharedSingleStatPanelChangedHandler)
if format, ok := angularOpts["format"].(string); ok {
defaults["unit"] = format
}
// Migrate decimals
if decimals, ok := angularOpts["decimals"]; ok {
defaults["decimals"] = decimals
}
// Note: Frontend migrateFromAngularSinglestat does migrate nullPointMode to nullValueMode
// but the frontend's getSaveModel() method removes it, so we don't add it here
// if nullPointMode, ok := angularOpts["nullPointMode"]; ok {
// defaults["nullValueMode"] = nullPointMode
// }
// Migrate null text
if nullText, ok := angularOpts["nullText"].(string); ok {
defaults["noValue"] = nullText
}
// Migrate value mappings (thresholds should already be migrated)
valueMaps, _ := angularOpts["valueMaps"].([]interface{})
migrateValueMappings(angularOpts, defaults, valueMaps)
// Migrate sparkline configuration
// Based on statPanelChangedHandler lines ~20-23: sparkline migration logic
if sparkline, ok := angularOpts["sparkline"].(map[string]interface{}); ok {
if show, ok := sparkline["show"].(bool); ok && show {
options["graphMode"] = "area"
} else {
options["graphMode"] = "none"
}
} else {
// Default to no graph mode if no sparkline configuration
options["graphMode"] = "none"
}
// Migrate color configuration
// Based on statPanelChangedHandler lines ~25-38: colorBackground and colorValue migration
colorMode := determineColorMode(angularOpts)
options["colorMode"] = colorMode
// Sparkline color migration only happens when colorMode is "none"
if colorMode == "none" {
migrateSparklineColor(angularOpts, defaults, options)
}
// Migrate text mode
// Based on statPanelChangedHandler lines ~45-47: valueName === 'name' migration
if valueName, ok := angularOpts["valueName"].(string); ok && valueName == "name" {
options["textMode"] = "name"
}
if angularOpts["gauge"] != nil && angularOpts["gauge"].(map[string]interface{})["show"] == true {
defaults["min"] = angularOpts["gauge"].(map[string]interface{})["minValue"]
defaults["max"] = angularOpts["gauge"].(map[string]interface{})["maxValue"]
}
}
// applyCompleteStatPanelDefaults applies the complete stat panel defaults
// This matches the frontend's getPanelOptionsWithDefaults behavior after migration
func applyCompleteStatPanelDefaults(panel map[string]interface{}) {
// Get or create options object
options, exists := panel["options"].(map[string]interface{})
if !exists {
options = map[string]interface{}{}
panel["options"] = options
}
defaultOptions := getDefaultStatOptions()
// Merge defaults with existing options, but don't override existing values
// This matches the frontend's getPanelOptionsWithDefaults behavior
for key, defaultValue := range defaultOptions {
if _, exists := options[key]; !exists {
options[key] = defaultValue
}
}
}
// applySharedSinglestatMigration applies shared migration logic for all singlestat panels
// Based on sharedSingleStatMigrationHandler in packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts
func applySharedSinglestatMigration(defaults map[string]interface{}) {
// Ensure thresholds have proper structure
if thresholds, ok := defaults["thresholds"].(map[string]interface{}); ok {
if steps, ok := thresholds["steps"].([]interface{}); ok {
// Ensure first threshold is -Infinity (represented as null in JSON)
if len(steps) > 0 {
if firstStep, ok := steps[0].(map[string]interface{}); ok {
if firstStep["value"] == nil {
firstStep["value"] = nil // Use null instead of -math.Inf(1)
}
}
}
}
}
// Handle percent/percentunit units
// Based on sharedSingleStatMigrationHandler lines ~280-300: percent/percentunit min/max handling
if unit, ok := defaults["unit"].(string); ok {
switch unit {
case "percent":
if defaults["min"] == nil {
defaults["min"] = 0
}
if defaults["max"] == nil {
defaults["max"] = 100
}
case "percentunit":
if defaults["min"] == nil {
defaults["min"] = 0
}
if defaults["max"] == nil {
defaults["max"] = 1
}
}
}
}
// Helper functions
func extractAngularOptions(panel map[string]interface{}) map[string]interface{} {
// Some panels might have angular options directly in the root
// Check for common angular properties
angularProps := []string{
"valueName", "tableColumn", "format", "decimals", "nullPointMode", "nullText",
"thresholds", "colors", "valueMaps", "gauge", "sparkline", "colorBackground", "colorValue",
}
for _, prop := range angularProps {
if _, exists := panel[prop]; exists {
return panel
}
}
return map[string]interface{}{}
}
// getReducerForValueName returns the mapped reducer or empty string for invalid values
func getReducerForValueName(valueName string) string {
reducerMap := map[string]string{
"min": "min",
"max": "max",
"mean": "mean",
"avg": "mean", // avg maps to mean
"median": "median",
"sum": "sum",
"count": "count",
"first": "firstNotNull",
"last": "lastNotNull",
"name": "lastNotNull",
"current": "lastNotNull",
"total": "sum",
}
if reducer, ok := reducerMap[valueName]; ok {
return reducer
}
return ""
}
func migrateThresholdsAndColors(defaults map[string]interface{}, thresholdsStr string, colors []interface{}) {
// Parse thresholds string (e.g., "10,20,30")
// Based on sharedSingleStatPanelChangedHandler lines ~145-165: Convert thresholds and color values
thresholds := []interface{}{}
thresholdValues := strings.Split(thresholdsStr, ",")
// Create threshold steps
for i, color := range colors {
step := map[string]interface{}{
"color": color,
}
if i == 0 {
// Frontend expects explicit null value for first step, not omitted field
// Use a pointer to ensure the field is present in JSON with null value
var nullValue *float64
step["value"] = nullValue
} else if i-1 < len(thresholdValues) {
if val, err := strconv.ParseFloat(strings.TrimSpace(thresholdValues[i-1]), 64); err == nil {
step["value"] = val
}
}
thresholds = append(thresholds, step)
}
defaults["thresholds"] = map[string]interface{}{
"mode": "absolute",
"steps": thresholds,
}
}
func migrateValueMappings(panel map[string]interface{}, defaults map[string]interface{}, valueMappings []interface{}) {
mappings := []interface{}{}
mappingType := panel["mappingType"]
// Check for inconsistent mapping configuration
// If panel has rangeMaps but mappingType is 1, or vice versa, fix it
hasValueMaps := panel["valueMaps"] != nil && IsArray(panel["valueMaps"]) && len(panel["valueMaps"].([]interface{})) > 0
hasRangeMaps := panel["rangeMaps"] != nil && IsArray(panel["rangeMaps"]) && len(panel["rangeMaps"].([]interface{})) > 0
if hasRangeMaps && mappingType == float64(1) {
mappingType = 2
} else if hasValueMaps && mappingType == float64(2) {
mappingType = 1
} else if mappingType == nil {
if hasValueMaps {
mappingType = 1
} else if hasRangeMaps {
mappingType = 2
}
}
switch mappingType {
case 1:
for _, valueMap := range valueMappings {
valueMapping := valueMap.(map[string]interface{})
upgradedMapping := upgradeOldAngularValueMapping(valueMapping, defaults["thresholds"])
if upgradedMapping != nil {
mappings = append(mappings, upgradedMapping)
}
}
case 2:
// Handle range mappings
if rangeMaps, ok := panel["rangeMaps"].([]interface{}); ok {
for _, rangeMap := range rangeMaps {
rangeMapping := rangeMap.(map[string]interface{})
upgradedMapping := upgradeOldAngularValueMapping(rangeMapping, defaults["thresholds"])
if upgradedMapping != nil {
mappings = append(mappings, upgradedMapping)
}
}
}
}
defaults["mappings"] = mappings
}
// upgradeOldAngularValueMapping converts old angular value mappings to new format
// Based on upgradeOldAngularValueMapping in packages/grafana-data/src/utils/valueMappings.ts
func upgradeOldAngularValueMapping(old map[string]interface{}, thresholds interface{}) map[string]interface{} {
valueMaps := map[string]interface{}{
"type": "value",
"options": map[string]interface{}{},
}
newMappings := []interface{}{}
// Use the color we would have picked from thresholds
// Frontend uses old.text to determine color, not old.value
var color interface{}
if text, ok := old["text"].(string); ok {
if numeric, err := parseNumericValue(text); err == nil {
if thresholdsMap, ok := thresholds.(map[string]interface{}); ok {
if steps, ok := thresholdsMap["steps"].([]interface{}); ok {
level := getActiveThreshold(numeric, steps)
if level != nil {
if levelColor, ok := level["color"]; ok {
color = levelColor
}
}
}
}
}
}
// Determine mapping type
mappingType := old["type"]
if mappingType == nil {
// Try to guess from available properties
if old["value"] != nil {
mappingType = 1 // ValueToText
} else if old["from"] != nil || old["to"] != nil {
mappingType = 2 // RangeToText
}
}
switch mappingType {
case 1: // ValueToText
if value, ok := old["value"]; ok && value != nil {
if valueStr, ok := value.(string); ok && valueStr == "null" {
newMappings = append(newMappings, map[string]interface{}{
"type": "special",
"options": map[string]interface{}{
"match": "null",
"result": map[string]interface{}{"text": old["text"], "color": color},
},
})
} else {
valueMaps["options"].(map[string]interface{})[fmt.Sprintf("%v", value)] = map[string]interface{}{
"text": old["text"],
"color": color,
}
}
}
case 2: // RangeToText
from := old["from"]
to := old["to"]
if (from != nil && fmt.Sprintf("%v", from) == "null") || (to != nil && fmt.Sprintf("%v", to) == "null") {
newMappings = append(newMappings, map[string]interface{}{
"type": "special",
"options": map[string]interface{}{
"match": "null",
"result": map[string]interface{}{"text": old["text"], "color": color},
},
})
} else {
var fromVal, toVal interface{}
if from != nil {
if fromStr, ok := from.(string); ok {
if fromFloat, err := strconv.ParseFloat(fromStr, 64); err == nil {
fromVal = fromFloat
}
} else {
fromVal = from
}
}
if to != nil {
if toStr, ok := to.(string); ok {
if toFloat, err := strconv.ParseFloat(toStr, 64); err == nil {
toVal = toFloat
}
} else {
toVal = to
}
}
newMappings = append(newMappings, map[string]interface{}{
"type": "range",
"options": map[string]interface{}{
"from": fromVal,
"to": toVal,
"result": map[string]interface{}{"text": old["text"], "color": color},
},
})
}
}
// Add valueMaps if it has options
if len(valueMaps["options"].(map[string]interface{})) > 0 {
newMappings = append([]interface{}{valueMaps}, newMappings...)
}
if len(newMappings) > 0 {
return newMappings[0].(map[string]interface{})
}
return nil
}
// getActiveThreshold finds the active threshold for a given value
// Based on getActiveThreshold in packages/grafana-data/src/field/thresholds.ts
func getActiveThreshold(value float64, steps []interface{}) map[string]interface{} {
for i := len(steps) - 1; i >= 0; i-- {
if step, ok := steps[i].(map[string]interface{}); ok {
if stepValue, ok := step["value"]; ok {
if stepValue == nil {
// First step with null value (represents -Infinity)
return step
}
if stepFloat, ok := stepValue.(float64); ok && value >= stepFloat {
return step
}
}
}
}
return nil
}
// parseNumericValue converts various types to float64 for threshold calculations
func parseNumericValue(value interface{}) (float64, error) {
switch v := value.(type) {
case string:
return strconv.ParseFloat(v, 64)
case float64:
return v, nil
case float32:
return float64(v), nil
case int:
return float64(v), nil
case int32:
return float64(v), nil
case int64:
return float64(v), nil
default:
return 0, fmt.Errorf("cannot convert %T to numeric value", value)
}
}
// createFieldConfigFromDefaults creates the proper fieldConfig structure from defaults
// and removes all legacy properties from the panel
func createFieldConfigFromDefaults(panel map[string]interface{}, defaults map[string]interface{}) {
// Ensure fieldConfig exists
if panel["fieldConfig"] == nil {
panel["fieldConfig"] = map[string]interface{}{
"defaults": map[string]interface{}{},
"overrides": []interface{}{},
}
}
fieldConfig := panel["fieldConfig"].(map[string]interface{})
fieldDefaults := fieldConfig["defaults"].(map[string]interface{})
// Copy all defaults to fieldConfig.defaults
for key, value := range defaults {
fieldDefaults[key] = value
}
// Note: Frontend doesn't add these extra fieldConfig defaults
// Color is handled in sparkline migration logic
// nullValueMode and unit are not added by frontend
// Remove all legacy properties from the panel
legacyProperties := []string{
"colors", "thresholds", "valueMaps", "grid", "legend", "mappingTypes", "gauge",
"autoMigrateFrom", "colorBackground", "colorValue", "format", "mappingType",
"nullPointMode", "postfix", "postfixFontSize", "prefix",
"prefixFontSize", "rangeMaps", "sparkline", "tableColumn", "valueFontSize",
"valueName", "aliasYAxis", "bars", "dashLength", "dashes", "fill", "fillGradient",
"lineInterpolation", "lineWidth", "pointRadius", "points", "spaceLength",
"stack", "steppedLine", "xAxis", "yAxes", "yAxis", "zIndex",
}
for _, prop := range legacyProperties {
delete(panel, prop)
}
}
// cleanupAngularProperties removes old angular properties after migration
// Based on PanelModel.clearPropertiesBeforePluginChange in public/app/features/dashboard/state/PanelModel.ts
// This function removes ALL properties except those in mustKeepProps to match frontend behavior exactly
func cleanupAngularProperties(panel map[string]interface{}) {
// Properties that must be kept (matching frontend mustKeepProps)
mustKeepProps := map[string]bool{
"id": true, "gridPos": true, "type": true, "title": true, "scopedVars": true,
"repeat": true, "repeatPanelId": true, "repeatDirection": true, "repeatedByRow": true,
"minSpan": true, "collapsed": true, "panels": true, "targets": true, "datasource": true,
"timeFrom": true, "timeShift": true, "hideTimeOverride": true, "description": true,
"links": true, "fullscreen": true, "isEditing": true, "isViewing": true,
"hasRefreshed": true, "events": true, "cacheTimeout": true, "queryCachingTTL": true,
"cachedPluginOptions": true, "transparent": true, "pluginVersion": true,
"fieldConfig": true, "options": true, // These are set by migration
"maxDataPoints": true, "interval": true, // Panel-level properties preserved by frontend
"autoMigrateFrom": true, // Preserve autoMigrateFrom for proper migration logic
}
// Remove ALL properties except those in mustKeepProps (matching frontend behavior)
for key := range panel {
if !mustKeepProps[key] {
delete(panel, key)
}
}
// Ensure all targets have refIds (matching frontend ensureQueryIds behavior)
ensureTargetRefIds(panel)
}
// ensureTargetRefIds assigns refIds to targets that don't have them
// This matches the frontend PanelModel.ensureQueryIds() behavior
func ensureTargetRefIds(panel map[string]interface{}) {
targets, ok := panel["targets"].([]interface{})
if !ok || len(targets) == 0 {
return
}
// Find existing refIds
existingRefIds := make(map[string]bool)
for _, targetInterface := range targets {
if target, ok := targetInterface.(map[string]interface{}); ok {
if refId, ok := target["refId"].(string); ok {
existingRefIds[refId] = true
}
}
}
// Assign refIds to targets that don't have them
letters := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
letterIndex := 0
for _, targetInterface := range targets {
if target, ok := targetInterface.(map[string]interface{}); ok {
refId, hasRefId := target["refId"].(string)
if !hasRefId || refId == "" {
// Find next available refId
for letterIndex < len(letters) {
refId := string(letters[letterIndex])
if !existingRefIds[refId] {
target["refId"] = refId
existingRefIds[refId] = true
break
}
letterIndex++
}
letterIndex++
}
}
}
}
// removeDeprecatedVariableProperties removes deprecated properties from variables
// Based on DashboardMigrator.ts v28 migration: variable property cleanup
func removeDeprecatedVariableProperties(variable map[string]interface{}) {
@ -843,45 +69,3 @@ func removeDeprecatedVariableProperties(variable map[string]interface{}) {
}
}
}
// determineColorMode determines the color mode based on angular options
func determineColorMode(angularOpts map[string]interface{}) string {
if colorBackground, ok := angularOpts["colorBackground"].(bool); ok && colorBackground {
return "background"
}
if colorValue, ok := angularOpts["colorValue"].(bool); ok && colorValue {
return "value"
}
return "none"
}
// migrateSparklineColor migrates sparkline color configuration when colorMode is "none"
// Based on statPanelChangedHandler lines 31-38
func migrateSparklineColor(angularOpts map[string]interface{}, defaults map[string]interface{}, options map[string]interface{}) {
sparkline, ok := angularOpts["sparkline"].(map[string]interface{})
if !ok {
return
}
show, ok := sparkline["show"].(bool)
if !ok || !show {
return
}
graphMode, ok := options["graphMode"].(string)
if !ok || graphMode != "area" {
return
}
lineColor, ok := sparkline["lineColor"].(string)
if !ok {
return
}
defaults["color"] = map[string]interface{}{
"mode": "fixed",
"fixedColor": lineColor,
}
}

View File

@ -5,101 +5,186 @@ import (
"testing"
)
func TestV28SinglestatMigration(t *testing.T) {
tests := []struct {
name string
input map[string]interface{}
expected map[string]interface{}
description string
}{
{
name: "migrate_range_maps_to_field_config_mappings",
input: map[string]interface{}{
"type": "singlestat",
"rangeMaps": []interface{}{
map[string]interface{}{
"from": "null",
"to": "N/A",
},
},
"mappingType": 1, // Inconsistent - should be 2 for rangeMaps
},
expected: map[string]interface{}{
"type": "stat",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"mappings": []interface{}{
map[string]interface{}{
"options": map[string]interface{}{
"match": "null",
"result": map[string]interface{}{
"text": "N/A",
},
},
"type": "special",
},
},
},
},
},
description: "RangeMaps should migrate to fieldConfig.mappings, and inconsistent mappingType should be fixed to 2 (RangeToText)",
},
{
name: "migrate_sparkline_color_when_color_mode_none",
input: map[string]interface{}{
"type": "singlestat",
"colorMode": "None",
"sparkline": map[string]interface{}{
"lineColor": "rgb(31, 120, 193)",
},
},
expected: map[string]interface{}{
"type": "stat",
"fieldConfig": map[string]interface{}{
"defaults": map[string]interface{}{
"color": map[string]interface{}{
"mode": "fixed",
"fixedColor": "rgb(31, 120, 193)",
},
},
},
},
description: "Sparkline lineColor should migrate to fieldConfig.defaults.color only when colorMode is None",
},
}
type migrationTestCase struct {
name string
input map[string]interface{}
expected map[string]interface{}
}
func runMigrationTests(t *testing.T, tests []migrationTestCase, migrationFunc func(context.Context, map[string]interface{}) error) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dashboard := map[string]interface{}{
"schemaVersion": 27,
"panels": []interface{}{tt.input},
// Create a copy of the input
dashboard := make(map[string]interface{})
for k, v := range tt.input {
dashboard[k] = v
}
err := V28(context.Background(), dashboard)
err := migrationFunc(context.Background(), dashboard)
if err != nil {
t.Fatalf("V28 migration failed: %v", err)
t.Fatalf("Migration failed: %v", err)
}
if dashboard["schemaVersion"] != 28 {
t.Errorf("Expected schemaVersion to be 28, got %v", dashboard["schemaVersion"])
// Verify the result matches expected
if !deepEqual(dashboard, tt.expected) {
t.Errorf("Migration result doesn't match expected.\nExpected: %+v\nGot: %+v", tt.expected, dashboard)
}
panels, ok := dashboard["panels"].([]interface{})
if !ok || len(panels) == 0 {
t.Fatalf("Expected panels array with at least one panel")
}
panel, ok := panels[0].(map[string]interface{})
if !ok {
t.Fatalf("Expected panel to be a map")
}
// Verify panel type was changed to stat
if panel["type"] != "stat" {
t.Errorf("Expected panel type to be 'stat', got %v", panel["type"])
}
t.Logf("✓ %s: %s", tt.name, tt.description)
})
}
}
func deepEqual(a, b interface{}) bool {
// Simple deep comparison for test purposes
// This is a simplified version - in production you'd use reflect.DeepEqual or similar
switch aVal := a.(type) {
case map[string]interface{}:
bVal, ok := b.(map[string]interface{})
if !ok || len(aVal) != len(bVal) {
return false
}
for k, v := range aVal {
if !deepEqual(v, bVal[k]) {
return false
}
}
return true
case []interface{}:
bVal, ok := b.([]interface{})
if !ok || len(aVal) != len(bVal) {
return false
}
for i, v := range aVal {
if !deepEqual(v, bVal[i]) {
return false
}
}
return true
default:
return a == b
}
}
func TestV28(t *testing.T) {
tests := []migrationTestCase{
{
name: "v28 removes deprecated variable properties",
input: map[string]interface{}{
"title": "V28 Variable Properties Migration Test Dashboard",
"schemaVersion": 27,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"name": "var1",
"tags": []interface{}{"tag1", "tag2"},
"tagsQuery": "query_string",
"tagValuesQuery": "values_query",
"useTags": true,
"type": "query",
},
map[string]interface{}{
"name": "var2",
"tags": []interface{}{},
"tagsQuery": "", // Empty string should not be removed
"tagValuesQuery": "", // Empty string should not be removed
"useTags": false, // False should not be removed
"type": "custom",
},
},
},
"panels": []interface{}{
map[string]interface{}{
"type": "singlestat",
"title": "Singlestat Panel (unchanged by v28)",
"id": 1,
},
},
},
expected: map[string]interface{}{
"title": "V28 Variable Properties Migration Test Dashboard",
"schemaVersion": 28,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"name": "var1",
"type": "query",
// tags, tagsQuery, tagValuesQuery, useTags should be removed
},
map[string]interface{}{
"name": "var2",
"tagsQuery": "", // Empty string preserved
"tagValuesQuery": "", // Empty string preserved
"useTags": false, // False preserved
"type": "custom",
// only tags should be removed
},
},
},
"panels": []interface{}{
map[string]interface{}{
"type": "singlestat",
"title": "Singlestat Panel (unchanged by v28)",
"id": 1,
},
},
},
},
{
name: "v28 handles dashboard without templating",
input: map[string]interface{}{
"title": "Dashboard without templating",
"schemaVersion": 27,
"panels": []interface{}{
map[string]interface{}{
"type": "singlestat",
"title": "Singlestat Panel",
"id": 1,
},
},
},
expected: map[string]interface{}{
"title": "Dashboard without templating",
"schemaVersion": 28,
"panels": []interface{}{
map[string]interface{}{
"type": "singlestat",
"title": "Singlestat Panel",
"id": 1,
},
},
},
},
{
name: "v28 handles empty templating list",
input: map[string]interface{}{
"title": "Dashboard with empty templating",
"schemaVersion": 27,
"templating": map[string]interface{}{
"list": []interface{}{},
},
"panels": []interface{}{
map[string]interface{}{
"type": "singlestat",
"title": "Singlestat Panel",
"id": 1,
},
},
},
expected: map[string]interface{}{
"title": "Dashboard with empty templating",
"schemaVersion": 28,
"templating": map[string]interface{}{
"list": []interface{}{},
},
"panels": []interface{}{
map[string]interface{}{
"type": "singlestat",
"title": "Singlestat Panel",
"id": 1,
},
},
},
},
}
runMigrationTests(t, tests, V28)
}

View File

@ -49,44 +49,26 @@
"type": "text"
},
{
"autoMigrateFrom": "singlestat",
"colorBackground": false,
"colorValue": false,
"colors": [
"#299c46",
"rgba(237, 129, 40, 0.89)",
"#d44a3a"
],
"datasource": {
"apiVersion": "v1",
"type": "grafana-testdata-datasource",
"uid": "testdata-type-uid"
},
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "rgb(31, 120, 193)",
"mode": "fixed"
},
"mappings": [
{
"options": {
"match": "null",
"result": {
"text": "N/A"
}
},
"type": "special"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "none"
},
"overrides": []
"format": "none",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 6,
@ -102,25 +84,37 @@
"url": "d/-Y-tnEDWk/dashboard-tests-nested-template-variables?orgId=1\u0026${__all_variables}\u0026${__url_time_range}"
}
],
"maxDataPoints": 100,
"options": {
"colorMode": "none",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"pluginVersion": "12.1.0",
"tableColumn": "",
"targets": [
{
"datasource": {
@ -132,8 +126,18 @@
"scenarioId": "random_walk"
}
],
"thresholds": "",
"title": "Panel drilldown link test",
"type": "stat"
"type": "stat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"aliasColors": {},

View File

@ -55,43 +55,31 @@
"type": "text"
},
{
"autoMigrateFrom": "singlestat",
"colorBackground": false,
"colorValue": false,
"colors": [
"#299c46",
"rgba(237, 129, 40, 0.89)",
"#d44a3a"
],
"datasource": {
"type": "grafana-testdata-datasource"
},
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "rgb(31, 120, 193)",
"mode": "fixed"
},
"mappings": [
{
"options": {
"match": "null",
"result": {
"text": "N/A"
}
},
"type": "special"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "none"
"custom": {}
},
"overrides": []
},
"format": "none",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 9,
"w": 4,
@ -106,25 +94,28 @@
"url": "d/O6GmNPvWk/dashboard-tests-nested-template-variables-drilldown?orgId=1\u0026${__all_variables}\u0026${__url_time_range}"
}
],
"mappingType": 1,
"mappingTypes": [],
"maxDataPoints": 100,
"options": {
"colorMode": "none",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
"nullPointMode": "connected",
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"pluginVersion": "12.1.0",
"tableColumn": "",
"targets": [
{
"datasource": {
@ -134,8 +125,18 @@
"scenarioId": "random_walk"
}
],
"thresholds": "",
"title": "Panel drilldown link test",
"type": "stat"
"type": "stat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"datasource": {

View File

@ -191,38 +191,24 @@
"type": "bargauge"
},
{
"autoMigrateFrom": "singlestat",
"colorBackground": false,
"colorValue": true,
"colors": [
"#299c46",
"#73BF69",
"#d44a3a"
],
"datasource": {
"type": "grafana-testdata-datasource"
},
"fieldConfig": {
"defaults": {
"mappings": [
{
"options": {
"match": "null",
"result": {
"text": "N/A"
}
},
"type": "special"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "ms"
},
"overrides": []
"format": "ms",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 4,
@ -231,25 +217,38 @@
"y": 7
},
"id": 20,
"maxDataPoints": 100,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"pluginVersion": "6.2.0-pre",
"postfix": "",
"postfixFontSize": "50%",
"prefix": "p99",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"pluginVersion": "12.1.0",
"tableColumn": "",
"targets": [
{
"datasource": {
@ -259,41 +258,37 @@
"scenarioId": "random_walk"
}
],
"type": "stat"
"thresholds": "",
"type": "stat",
"valueFontSize": "120%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"autoMigrateFrom": "singlestat",
"colorBackground": false,
"colorValue": true,
"colors": [
"#299c46",
"#73BF69",
"#d44a3a"
],
"datasource": {
"type": "grafana-testdata-datasource"
},
"fieldConfig": {
"defaults": {
"mappings": [
{
"options": {
"match": "null",
"result": {
"text": "N/A"
}
},
"type": "special"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "ms"
},
"overrides": []
"format": "ms",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 4,
@ -302,25 +297,38 @@
"y": 7
},
"id": 23,
"maxDataPoints": 100,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"pluginVersion": "6.2.0-pre",
"postfix": "",
"postfixFontSize": "50%",
"prefix": "p95",
"prefixFontSize": "80%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"pluginVersion": "12.1.0",
"tableColumn": "",
"targets": [
{
"datasource": {
@ -330,41 +338,37 @@
"scenarioId": "random_walk"
}
],
"type": "stat"
"thresholds": "",
"type": "stat",
"valueFontSize": "120%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"autoMigrateFrom": "singlestat",
"colorBackground": false,
"colorValue": true,
"colors": [
"#299c46",
"#73BF69",
"#d44a3a"
],
"datasource": {
"type": "grafana-testdata-datasource"
},
"fieldConfig": {
"defaults": {
"mappings": [
{
"options": {
"match": "null",
"result": {
"text": "N/A"
}
},
"type": "special"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "ms"
},
"overrides": []
"format": "ms",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 4,
@ -373,25 +377,38 @@
"y": 7
},
"id": 24,
"maxDataPoints": 100,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"pluginVersion": "6.2.0-pre",
"postfix": "",
"postfixFontSize": "50%",
"prefix": "p90",
"prefixFontSize": "80%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"pluginVersion": "12.1.0",
"tableColumn": "",
"targets": [
{
"datasource": {
@ -401,41 +418,37 @@
"scenarioId": "random_walk"
}
],
"type": "stat"
"thresholds": "",
"type": "stat",
"valueFontSize": "120%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"autoMigrateFrom": "singlestat",
"colorBackground": false,
"colorValue": true,
"colors": [
"#299c46",
"#73BF69",
"#d44a3a"
],
"datasource": {
"type": "grafana-testdata-datasource"
},
"fieldConfig": {
"defaults": {
"mappings": [
{
"options": {
"match": "null",
"result": {
"text": "N/A"
}
},
"type": "special"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "ms"
},
"overrides": []
"format": "ms",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 4,
@ -444,25 +457,38 @@
"y": 7
},
"id": 45,
"maxDataPoints": 100,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"pluginVersion": "6.2.0-pre",
"postfix": "",
"postfixFontSize": "50%",
"prefix": "p90",
"prefixFontSize": "80%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"pluginVersion": "12.1.0",
"tableColumn": "",
"targets": [
{
"datasource": {
@ -472,7 +498,17 @@
"scenarioId": "random_walk"
}
],
"type": "stat"
"thresholds": "",
"type": "stat",
"valueFontSize": "120%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"aliasColors": {

View File

@ -20,45 +20,27 @@
"links": [],
"panels": [
{
"autoMigrateFrom": "singlestat",
"colorBackground": false,
"colorValue": false,
"colors": [
"#299c46",
"#5794F2",
"#d44a3a"
],
"datasource": {
"apiVersion": "v1",
"type": "grafana-testdata-datasource",
"uid": "testdata-type-uid"
},
"description": "asdasdas",
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "rgb(31, 120, 193)",
"mode": "fixed"
},
"mappings": [
{
"options": {
"match": "null",
"result": {
"text": "N/A"
}
},
"type": "special"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "ms"
},
"overrides": []
"format": "ms",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 4,
@ -67,25 +49,37 @@
"y": 0
},
"id": 8,
"maxDataPoints": 100,
"options": {
"colorMode": "none",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "100%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": true,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"pluginVersion": "12.1.0",
"tableColumn": "",
"targets": [
{
"datasource": {
@ -97,50 +91,42 @@
"scenarioId": "random_walk"
}
],
"thresholds": "",
"timeShift": "2h",
"title": "Title",
"type": "stat"
"type": "stat",
"valueFontSize": "120%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"autoMigrateFrom": "singlestat",
"colorBackground": false,
"colorValue": false,
"colors": [
"#299c46",
"#5794F2",
"#d44a3a"
],
"datasource": {
"apiVersion": "v1",
"type": "grafana-testdata-datasource",
"uid": "testdata-type-uid"
},
"description": "asdasdas",
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "rgb(31, 120, 193)",
"mode": "fixed"
},
"mappings": [
{
"options": {
"match": "null",
"result": {
"text": "N/A"
}
},
"type": "special"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "ms"
},
"overrides": []
"format": "ms",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 4,
@ -149,25 +135,37 @@
"y": 0
},
"id": 2,
"maxDataPoints": 100,
"options": {
"colorMode": "none",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "100%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": true,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"pluginVersion": "12.1.0",
"tableColumn": "",
"targets": [
{
"datasource": {
@ -179,49 +177,41 @@
"scenarioId": "random_walk"
}
],
"thresholds": "",
"timeShift": "2h",
"type": "stat"
"type": "stat",
"valueFontSize": "120%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"autoMigrateFrom": "singlestat",
"colorBackground": false,
"colorValue": false,
"colors": [
"#299c46",
"#5794F2",
"#d44a3a"
],
"datasource": {
"apiVersion": "v1",
"type": "grafana-testdata-datasource",
"uid": "testdata-type-uid"
},
"description": "asdasdas",
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "rgb(31, 120, 193)",
"mode": "fixed"
},
"mappings": [
{
"options": {
"match": "null",
"result": {
"text": "N/A"
}
},
"type": "special"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "ms"
},
"overrides": []
"format": "ms",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 4,
@ -230,25 +220,37 @@
"y": 4
},
"id": 4,
"maxDataPoints": 100,
"options": {
"colorMode": "none",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "100%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": true,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"pluginVersion": "12.1.0",
"tableColumn": "",
"targets": [
{
"datasource": {
@ -260,9 +262,19 @@
"scenarioId": "random_walk"
}
],
"thresholds": "",
"timeShift": "2h",
"title": "Panel Title",
"type": "stat"
"type": "stat",
"valueFontSize": "120%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"datasource": {
@ -404,43 +416,27 @@
"type": "text"
},
{
"autoMigrateFrom": "singlestat",
"colorBackground": false,
"colorValue": false,
"colors": [
"#299c46",
"rgba(237, 129, 40, 0.89)",
"#d44a3a"
],
"datasource": {
"apiVersion": "v1",
"type": "grafana-testdata-datasource",
"uid": "testdata-type-uid"
},
"description": "asdasdas",
"fieldConfig": {
"defaults": {
"mappings": [
{
"options": {
"match": "null",
"result": {
"text": "N/A"
}
},
"type": "special"
}
],
"max": 100,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "none"
},
"overrides": []
"format": "none",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": true,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 4,
@ -449,25 +445,38 @@
"y": 8
},
"id": 10,
"maxDataPoints": 100,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"pluginVersion": "6.2.0-pre",
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"pluginVersion": "12.1.0",
"tableColumn": "",
"targets": [
{
"datasource": {
@ -479,8 +488,18 @@
"scenarioId": "random_walk"
}
],
"thresholds": "",
"timeShift": "2h",
"type": "stat"
"type": "stat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"datasource": {

View File

@ -136,49 +136,13 @@
"type": "timeseries"
},
{
"autoMigrateFrom": "singlestat",
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"id": 5,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"targets": [
{
"datasource": {

View File

@ -54,59 +54,13 @@
]
},
{
"autoMigrateFrom": "singlestat",
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"fieldConfig": {
"defaults": {
"mappings": [
{
"options": {
"match": "null",
"result": {
"text": "N/A"
}
},
"type": "special"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"id": 2,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"targets": [
{
"datasource": {
@ -118,7 +72,14 @@
}
],
"title": "Memory Usage",
"type": "stat"
"type": "stat",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
]
}
],
"refresh": "",

View File

@ -75,30 +75,12 @@
"type": "timeseries"
},
{
"autoMigrateFrom": "singlestat",
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
@ -106,24 +88,6 @@
"y": 1
},
"id": 2,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"targets": [
{
"datasource": {

View File

@ -77,30 +77,12 @@
"type": "timeseries"
},
{
"autoMigrateFrom": "singlestat",
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 6,
@ -108,24 +90,7 @@
"y": 8
},
"id": 2,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"maxPerRow": 6,
"targets": [
{
"datasource": {

View File

@ -153,49 +153,13 @@
"type": "gauge"
},
{
"autoMigrateFrom": "singlestat",
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"id": 6,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"targets": [
{
"datasource": {

View File

@ -1226,50 +1226,14 @@
"type": "timeseries"
},
{
"autoMigrateFrom": "singlestat",
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"description": "Other panel types should not be affected by table migration.",
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"id": 17,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"targets": [
{
"datasource": {

View File

@ -21,53 +21,23 @@
"links": [],
"panels": [
{
"autoMigrateFrom": "singlestat",
"colors": [
"#FF0000",
"green",
"orange"
],
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "#FF0000",
"value": null
},
{
"color": "green",
"value": 10
},
{
"color": "orange",
"value": 20
}
]
}
},
"overrides": []
"grid": {
"max": 10,
"min": 1
},
"id": 1,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"legend": true,
"targets": [
{
"datasource": {
@ -86,56 +56,31 @@
"refId": "B"
}
],
"thresholds": "10,20,30",
"type": "stat"
},
{
"autoMigrateFrom": "singlestat",
"colors": [
"#FF0000",
"green",
"orange"
],
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "#FF0000",
"value": null
},
{
"color": "green",
"value": 10
},
{
"color": "orange",
"value": 20
}
]
}
},
"overrides": []
"gauge": {
"show": true,
"thresholdLabels": false,
"thresholdMarkers": true
},
"grid": {
"max": 10,
"min": 1
},
"id": 2,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"targets": [
{
"datasource": {
@ -146,82 +91,33 @@
"refId": "A"
}
],
"thresholds": "10,20,30",
"type": "stat"
},
{
"autoMigrateFrom": "singlestat",
"colors": [
"#FF0000",
"green",
"orange"
],
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"fieldConfig": {
"defaults": {
"mappings": [
{
"options": {
"20": {
"text": "test"
}
},
"type": "value"
},
{
"options": {
"30": {
"text": "test1"
}
},
"type": "value"
},
{
"options": {
"40": {
"color": "orange",
"text": "50"
}
},
"type": "value"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "#FF0000",
"value": null
},
{
"color": "green",
"value": 10
},
{
"color": "orange",
"value": 20
}
]
}
},
"overrides": []
"grid": {
"max": 10,
"min": 1
},
"id": 3,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"legend": true,
"mappingTypes": [
{
"name": "value to text",
"value": 1
}
],
"targets": [
{
"datasource": {
@ -240,7 +136,25 @@
"refId": "B"
}
],
"type": "stat"
"thresholds": "10,20,30",
"type": "stat",
"valueMaps": [
{
"op": "=",
"text": "test",
"value": "20"
},
{
"op": "=",
"text": "test1",
"value": "30"
},
{
"op": "=",
"text": "50",
"value": "40"
}
]
},
{
"datasource": {

View File

@ -21,53 +21,23 @@
"links": [],
"panels": [
{
"autoMigrateFrom": "singlestat",
"colors": [
"#FF0000",
"green",
"orange"
],
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "#FF0000",
"value": null
},
{
"color": "green",
"value": 10
},
{
"color": "orange",
"value": 20
}
]
}
},
"overrides": []
"grid": {
"max": 10,
"min": 1
},
"id": 1,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"legend": true,
"targets": [
{
"datasource": {
@ -86,52 +56,27 @@
"refId": "B"
}
],
"thresholds": "10,20,30",
"type": "stat"
},
{
"autoMigrateFrom": "singlestat",
"colors": [
"#FF0000",
"green",
"orange"
],
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
"grid": {
"max": 10,
"min": 1
},
"id": 2,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"legend": true,
"targets": [
{
"datasource": {
@ -150,56 +95,31 @@
"refId": "B"
}
],
"thresholds": "",
"type": "stat"
},
{
"autoMigrateFrom": "singlestat",
"colors": [
"#FF0000",
"green",
"orange"
],
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "#FF0000",
"value": null
},
{
"color": "green",
"value": 10
},
{
"color": "orange",
"value": 20
}
]
}
},
"overrides": []
"gauge": {
"show": true,
"thresholdLabels": false,
"thresholdMarkers": true
},
"grid": {
"max": 10,
"min": 1
},
"id": 3,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"targets": [
{
"datasource": {
@ -210,82 +130,33 @@
"refId": "A"
}
],
"thresholds": "10,20,30",
"type": "stat"
},
{
"autoMigrateFrom": "singlestat",
"colors": [
"#FF0000",
"green",
"orange"
],
"datasource": {
"apiVersion": "v1",
"type": "prometheus",
"uid": "default-ds-uid"
},
"fieldConfig": {
"defaults": {
"mappings": [
{
"options": {
"20": {
"text": "test"
}
},
"type": "value"
},
{
"options": {
"30": {
"text": "test1"
}
},
"type": "value"
},
{
"options": {
"40": {
"color": "orange",
"text": "50"
}
},
"type": "value"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "#FF0000",
"value": null
},
{
"color": "green",
"value": 10
},
{
"color": "orange",
"value": 20
}
]
}
},
"overrides": []
"grid": {
"max": 10,
"min": 1
},
"id": 4,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"legend": true,
"mappingTypes": [
{
"name": "value to text",
"value": 1
}
],
"targets": [
{
"datasource": {
@ -304,7 +175,25 @@
"refId": "B"
}
],
"type": "stat"
"thresholds": "10,20,30",
"type": "stat",
"valueMaps": [
{
"op": "=",
"text": "test",
"value": "20"
},
{
"op": "=",
"text": "test1",
"value": "30"
},
{
"op": "=",
"text": "50",
"value": "40"
}
]
},
{
"datasource": {
@ -327,38 +216,24 @@
"type": "timeseries"
},
{
"autoMigrateFrom": "grafana-singlestat-panel",
"colorBackground": false,
"colorValue": true,
"colors": [
"#299c46",
"rgba(237, 129, 40, 0.89)",
"#d44a3a"
],
"datasource": {
"type": "prometheus"
},
"fieldConfig": {
"defaults": {
"mappings": [
{
"options": {
"match": "null",
"result": {
"text": "N/A"
}
},
"type": "special"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "areaF2"
},
"overrides": []
"format": "areaF2",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 8,
@ -367,25 +242,37 @@
"y": 43
},
"id": 5,
"maxDataPoints": 100,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"postfix": "b",
"postfixFontSize": "50%",
"prefix": "a",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"pluginVersion": "12.1.0",
"tableColumn": "",
"targets": [
{
"datasource": {
@ -395,8 +282,18 @@
"refId": "A"
}
],
"thresholds": "",
"title": "grafana-singlestat-panel",
"type": "stat"
"type": "stat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"datasource": {

View File

@ -21,48 +21,18 @@
"links": [],
"panels": [
{
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "#FF0000",
"value": null
},
{
"color": "green",
"value": 10
},
{
"color": "orange",
"value": 20
}
]
}
},
"overrides": []
"autoMigrateFrom": "singlestat",
"colors": [
"#FF0000",
"green",
"orange"
],
"grid": {
"max": 10,
"min": 1
},
"id": 1,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"legend": true,
"targets": [
{
"refId": "A"
@ -71,122 +41,48 @@
"refId": "B"
}
],
"thresholds": "10,20,30",
"type": "stat"
},
{
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "#FF0000",
"value": null
},
{
"color": "green",
"value": 10
},
{
"color": "orange",
"value": 20
}
]
}
},
"overrides": []
"autoMigrateFrom": "singlestat",
"colors": [
"#FF0000",
"green",
"orange"
],
"gauge": {
"show": true,
"thresholdLabels": false,
"thresholdMarkers": true
},
"grid": {
"max": 10,
"min": 1
},
"id": 2,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"thresholds": "10,20,30",
"type": "stat"
},
{
"fieldConfig": {
"defaults": {
"mappings": [
{
"options": {
"20": {
"text": "test"
}
},
"type": "value"
},
{
"options": {
"30": {
"text": "test1"
}
},
"type": "value"
},
{
"options": {
"40": {
"color": "orange",
"text": "50"
}
},
"type": "value"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "#FF0000",
"value": null
},
{
"color": "green",
"value": 10
},
{
"color": "orange",
"value": 20
}
]
}
},
"overrides": []
"autoMigrateFrom": "singlestat",
"colors": [
"#FF0000",
"green",
"orange"
],
"grid": {
"max": 10,
"min": 1
},
"id": 3,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"legend": true,
"mappingTypes": [
{
"name": "value to text",
"value": 1
}
],
"targets": [
{
"refId": "A"
@ -195,7 +91,25 @@
"refId": "B"
}
],
"type": "stat"
"thresholds": "10,20,30",
"type": "stat",
"valueMaps": [
{
"op": "=",
"text": "test",
"value": "20"
},
{
"op": "=",
"text": "test1",
"value": "30"
},
{
"op": "=",
"text": "50",
"value": "40"
}
]
},
{
"id": 4,

View File

@ -21,48 +21,18 @@
"links": [],
"panels": [
{
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "#FF0000",
"value": null
},
{
"color": "green",
"value": 10
},
{
"color": "orange",
"value": 20
}
]
}
},
"overrides": []
"autoMigrateFrom": "singlestat",
"colors": [
"#FF0000",
"green",
"orange"
],
"grid": {
"max": 10,
"min": 1
},
"id": 1,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"legend": true,
"targets": [
{
"refId": "A"
@ -71,47 +41,22 @@
"refId": "B"
}
],
"thresholds": "10,20,30",
"type": "stat"
},
{
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
"autoMigrateFrom": "singlestat",
"colors": [
"#FF0000",
"green",
"orange"
],
"grid": {
"max": 10,
"min": 1
},
"id": 2,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"legend": true,
"targets": [
{
"refId": "A"
@ -120,122 +65,48 @@
"refId": "B"
}
],
"thresholds": "",
"type": "stat"
},
{
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "#FF0000",
"value": null
},
{
"color": "green",
"value": 10
},
{
"color": "orange",
"value": 20
}
]
}
},
"overrides": []
"autoMigrateFrom": "singlestat",
"colors": [
"#FF0000",
"green",
"orange"
],
"gauge": {
"show": true,
"thresholdLabels": false,
"thresholdMarkers": true
},
"grid": {
"max": 10,
"min": 1
},
"id": 3,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"thresholds": "10,20,30",
"type": "stat"
},
{
"fieldConfig": {
"defaults": {
"mappings": [
{
"options": {
"20": {
"text": "test"
}
},
"type": "value"
},
{
"options": {
"30": {
"text": "test1"
}
},
"type": "value"
},
{
"options": {
"40": {
"color": "orange",
"text": "50"
}
},
"type": "value"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "#FF0000",
"value": null
},
{
"color": "green",
"value": 10
},
{
"color": "orange",
"value": 20
}
]
}
},
"overrides": []
"autoMigrateFrom": "singlestat",
"colors": [
"#FF0000",
"green",
"orange"
],
"grid": {
"max": 10,
"min": 1
},
"id": 4,
"options": {
"colorMode": "none",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0",
"legend": true,
"mappingTypes": [
{
"name": "value to text",
"value": 1
}
],
"targets": [
{
"refId": "A"
@ -244,7 +115,25 @@
"refId": "B"
}
],
"type": "stat"
"thresholds": "10,20,30",
"type": "stat",
"valueMaps": [
{
"op": "=",
"text": "test",
"value": "20"
},
{
"op": "=",
"text": "test1",
"value": "30"
},
{
"op": "=",
"text": "50",
"value": "40"
}
]
},
{
"id": 8,
@ -257,38 +146,24 @@
"type": "timeseries"
},
{
"autoMigrateFrom": "grafana-singlestat-panel",
"colorBackground": false,
"colorValue": true,
"colors": [
"#299c46",
"rgba(237, 129, 40, 0.89)",
"#d44a3a"
],
"datasource": {
"type": "prometheus"
},
"fieldConfig": {
"defaults": {
"mappings": [
{
"options": {
"match": "null",
"result": {
"text": "N/A"
}
},
"type": "special"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "areaF2"
},
"overrides": []
"format": "areaF2",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 8,
@ -297,25 +172,37 @@
"y": 43
},
"id": 5,
"maxDataPoints": 100,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"postfix": "b",
"postfixFontSize": "50%",
"prefix": "a",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"pluginVersion": "12.1.0",
"tableColumn": "",
"targets": [
{
"datasource": {
@ -325,8 +212,18 @@
"refId": "A"
}
],
"thresholds": "",
"title": "grafana-singlestat-panel",
"type": "stat"
"type": "stat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"datasource": {

View File

@ -240,10 +240,10 @@ export async function handleAngularPanelMigration(
We need to manually run the pluginLoaded logic to ensure the panels are migrated correctly.
*/
for (const panel of frontendModel.panels) {
if (panel.type === 'stat' && panel.autoMigrateFrom && targetVersion >= 28 && sourceVersion < 28) {
const statPlugin = getPanelPlugin('stat');
await panel.pluginLoaded(statPlugin);
}
// if (panel.type === 'stat' && panel.autoMigrateFrom && targetVersion >= 28 && sourceVersion < 28) {
// const statPlugin = getPanelPlugin('stat');
// await panel.pluginLoaded(statPlugin);
// }
if (panel.type === 'table' && panel.autoMigrateFrom === 'table-old' && targetVersion >= 24 && sourceVersion < 24) {
const tablePlugin = getPanelPlugin('table');
await panel.pluginLoaded(tablePlugin);