Schema v2: Move annotation mappings out of legacyOptions (#114721)
* Add mappings property to v2(alpha/beta)1 * Mappings convetions * OpenAPI spec * fe annotaations mapping * Update snapshots * v2->v1 conversion * fix tests --------- Co-authored-by: Haris Rozajac <haris.rozajac12@gmail.com>
This commit is contained in:
co-authored by
Haris Rozajac
parent
3e50bfced8
commit
bb787a2637
@@ -81,6 +81,22 @@ AnnotationPanelFilter: {
|
||||
ids: [...uint32]
|
||||
}
|
||||
|
||||
// Annotation event field source. Defines how to obtain the value for an annotation event field.
|
||||
// - "field": Find the value with a matching key (default)
|
||||
// - "text": Write a constant string into the value
|
||||
// - "skip": Do not include the field
|
||||
AnnotationEventFieldSource: "field" | "text" | "skip" | *"field"
|
||||
|
||||
// Annotation event field mapping. Defines how to map a data frame field to an annotation event field.
|
||||
AnnotationEventFieldMapping: {
|
||||
// Source type for the field value
|
||||
source?: AnnotationEventFieldSource | *"field"
|
||||
// Constant value to use when source is "text"
|
||||
value?: string
|
||||
// Regular expression to apply to the field value
|
||||
regex?: string
|
||||
}
|
||||
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
// "Crosshair" for shared crosshair.
|
||||
// "Tooltip" for shared crosshair AND shared tooltip.
|
||||
@@ -451,7 +467,10 @@ AnnotationQuerySpec: {
|
||||
name: string
|
||||
builtIn?: bool | *false
|
||||
filter?: AnnotationPanelFilter
|
||||
legacyOptions?: [string]: _ //Catch-all field for datasource-specific properties
|
||||
// Mappings define how to convert data frame fields to annotation event fields.
|
||||
mappings?: [string]: AnnotationEventFieldMapping
|
||||
// Catch-all field for datasource-specific properties
|
||||
legacyOptions?: [string]: _
|
||||
}
|
||||
|
||||
AnnotationQueryKind: {
|
||||
|
||||
@@ -75,10 +75,26 @@ LibraryPanelRef: {
|
||||
|
||||
AnnotationPanelFilter: {
|
||||
// Should the specified panels be included or excluded
|
||||
exclude?: bool | *false
|
||||
exclude?: bool | *false
|
||||
|
||||
// Panel IDs that should be included or excluded
|
||||
ids: [...uint32]
|
||||
}
|
||||
|
||||
// Annotation event field source. Defines how to obtain the value for an annotation event field.
|
||||
// - "field": Find the value with a matching key (default)
|
||||
// - "text": Write a constant string into the value
|
||||
// - "skip": Do not include the field
|
||||
AnnotationEventFieldSource: "field" | "text" | "skip" | *"field"
|
||||
|
||||
// Annotation event field mapping. Defines how to map a data frame field to an annotation event field.
|
||||
AnnotationEventFieldMapping: {
|
||||
// Source type for the field value
|
||||
source?: AnnotationEventFieldSource | *"field"
|
||||
// Constant value to use when source is "text"
|
||||
value?: string
|
||||
// Regular expression to apply to the field value
|
||||
regex?: string
|
||||
}
|
||||
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
@@ -450,7 +466,10 @@ AnnotationQuerySpec: {
|
||||
filter?: AnnotationPanelFilter
|
||||
// Placement can be used to display the annotation query somewhere else on the dashboard other than the default location.
|
||||
placement?: AnnotationQueryPlacement
|
||||
legacyOptions?: [string]: _ // Catch-all field for datasource-specific properties. Should not be available in as code tooling.
|
||||
// Mappings define how to convert data frame fields to annotation event fields.
|
||||
mappings?: [string]: AnnotationEventFieldMapping
|
||||
// Catch-all field for datasource-specific properties. Should not be available in as code tooling.
|
||||
legacyOptions?: [string]: _
|
||||
}
|
||||
|
||||
AnnotationQueryKind: {
|
||||
|
||||
@@ -85,6 +85,22 @@ AnnotationPanelFilter: {
|
||||
ids: [...uint32]
|
||||
}
|
||||
|
||||
// Annotation event field source. Defines how to obtain the value for an annotation event field.
|
||||
// - "field": Find the value with a matching key (default)
|
||||
// - "text": Write a constant string into the value
|
||||
// - "skip": Do not include the field
|
||||
AnnotationEventFieldSource: "field" | "text" | "skip" | *"field"
|
||||
|
||||
// Annotation event field mapping. Defines how to map a data frame field to an annotation event field.
|
||||
AnnotationEventFieldMapping: {
|
||||
// Source type for the field value
|
||||
source?: AnnotationEventFieldSource | *"field"
|
||||
// Constant value to use when source is "text"
|
||||
value?: string
|
||||
// Regular expression to apply to the field value
|
||||
regex?: string
|
||||
}
|
||||
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
// "Crosshair" for shared crosshair.
|
||||
// "Tooltip" for shared crosshair AND shared tooltip.
|
||||
@@ -455,7 +471,10 @@ AnnotationQuerySpec: {
|
||||
name: string
|
||||
builtIn?: bool | *false
|
||||
filter?: AnnotationPanelFilter
|
||||
legacyOptions?: [string]: _ //Catch-all field for datasource-specific properties
|
||||
// Mappings define how to convert data frame fields to annotation event fields.
|
||||
mappings?: [string]: AnnotationEventFieldMapping
|
||||
// Catch-all field for datasource-specific properties
|
||||
legacyOptions?: [string]: _
|
||||
}
|
||||
|
||||
AnnotationQueryKind: {
|
||||
|
||||
@@ -31,6 +31,8 @@ type DashboardAnnotationQuerySpec struct {
|
||||
Name string `json:"name"`
|
||||
BuiltIn *bool `json:"builtIn,omitempty"`
|
||||
Filter *DashboardAnnotationPanelFilter `json:"filter,omitempty"`
|
||||
// Mappings define how to convert data frame fields to annotation event fields.
|
||||
Mappings map[string]DashboardAnnotationEventFieldMapping `json:"mappings,omitempty"`
|
||||
// Catch-all field for datasource-specific properties
|
||||
LegacyOptions map[string]interface{} `json:"legacyOptions,omitempty"`
|
||||
}
|
||||
@@ -85,6 +87,24 @@ func NewDashboardAnnotationPanelFilter() *DashboardAnnotationPanelFilter {
|
||||
}
|
||||
}
|
||||
|
||||
// Annotation event field mapping. Defines how to map a data frame field to an annotation event field.
|
||||
// +k8s:openapi-gen=true
|
||||
type DashboardAnnotationEventFieldMapping struct {
|
||||
// Source type for the field value
|
||||
Source *string `json:"source,omitempty"`
|
||||
// Constant value to use when source is "text"
|
||||
Value *string `json:"value,omitempty"`
|
||||
// Regular expression to apply to the field value
|
||||
Regex *string `json:"regex,omitempty"`
|
||||
}
|
||||
|
||||
// NewDashboardAnnotationEventFieldMapping creates a new DashboardAnnotationEventFieldMapping object.
|
||||
func NewDashboardAnnotationEventFieldMapping() *DashboardAnnotationEventFieldMapping {
|
||||
return &DashboardAnnotationEventFieldMapping{
|
||||
Source: (func(input string) *string { return &input })("field"),
|
||||
}
|
||||
}
|
||||
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
// "Crosshair" for shared crosshair.
|
||||
// "Tooltip" for shared crosshair AND shared tooltip.
|
||||
|
||||
@@ -23,6 +23,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdHocFilterWithLabels": schema_pkg_apis_dashboard_v2alpha1_DashboardAdHocFilterWithLabels(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableKind(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableSpec(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationEventFieldMapping": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationEventFieldMapping(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationPanelFilter": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationPanelFilter(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQueryKind(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQuerySpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQuerySpec(ref),
|
||||
@@ -644,6 +645,40 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableSpec(ref common.Re
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationEventFieldMapping(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Annotation event field mapping. Defines how to map a data frame field to an annotation event field.",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"source": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Source type for the field value",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"value": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Constant value to use when source is \"text\"",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"regex": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Regular expression to apply to the field value",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationPanelFilter(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
@@ -762,6 +797,21 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQuerySpec(ref common.
|
||||
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationPanelFilter"),
|
||||
},
|
||||
},
|
||||
"mappings": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Mappings define how to convert data frame fields to annotation event fields.",
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationEventFieldMapping"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"legacyOptions": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Catch-all field for datasource-specific properties",
|
||||
@@ -782,7 +832,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQuerySpec(ref common.
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationPanelFilter", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataSourceRef"},
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationEventFieldMapping", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationPanelFilter", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataSourceRef"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,10 +79,26 @@ LibraryPanelRef: {
|
||||
|
||||
AnnotationPanelFilter: {
|
||||
// Should the specified panels be included or excluded
|
||||
exclude?: bool | *false
|
||||
exclude?: bool | *false
|
||||
|
||||
// Panel IDs that should be included or excluded
|
||||
ids: [...uint32]
|
||||
}
|
||||
|
||||
// Annotation event field source. Defines how to obtain the value for an annotation event field.
|
||||
// - "field": Find the value with a matching key (default)
|
||||
// - "text": Write a constant string into the value
|
||||
// - "skip": Do not include the field
|
||||
AnnotationEventFieldSource: "field" | "text" | "skip" | *"field"
|
||||
|
||||
// Annotation event field mapping. Defines how to map a data frame field to an annotation event field.
|
||||
AnnotationEventFieldMapping: {
|
||||
// Source type for the field value
|
||||
source?: AnnotationEventFieldSource | *"field"
|
||||
// Constant value to use when source is "text"
|
||||
value?: string
|
||||
// Regular expression to apply to the field value
|
||||
regex?: string
|
||||
}
|
||||
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
@@ -454,7 +470,10 @@ AnnotationQuerySpec: {
|
||||
filter?: AnnotationPanelFilter
|
||||
// Placement can be used to display the annotation query somewhere else on the dashboard other than the default location.
|
||||
placement?: AnnotationQueryPlacement
|
||||
legacyOptions?: [string]: _ // Catch-all field for datasource-specific properties. Should not be available in as code tooling.
|
||||
// Mappings define how to convert data frame fields to annotation event fields.
|
||||
mappings?: [string]: AnnotationEventFieldMapping
|
||||
// Catch-all field for datasource-specific properties. Should not be available in as code tooling.
|
||||
legacyOptions?: [string]: _
|
||||
}
|
||||
|
||||
AnnotationQueryKind: {
|
||||
|
||||
@@ -32,6 +32,8 @@ type DashboardAnnotationQuerySpec struct {
|
||||
Filter *DashboardAnnotationPanelFilter `json:"filter,omitempty"`
|
||||
// Placement can be used to display the annotation query somewhere else on the dashboard other than the default location.
|
||||
Placement *string `json:"placement,omitempty"`
|
||||
// Mappings define how to convert data frame fields to annotation event fields.
|
||||
Mappings map[string]DashboardAnnotationEventFieldMapping `json:"mappings,omitempty"`
|
||||
// Catch-all field for datasource-specific properties. Should not be available in as code tooling.
|
||||
LegacyOptions map[string]interface{} `json:"legacyOptions,omitempty"`
|
||||
}
|
||||
@@ -86,6 +88,24 @@ func NewDashboardAnnotationPanelFilter() *DashboardAnnotationPanelFilter {
|
||||
// +k8s:openapi-gen=true
|
||||
const DashboardAnnotationQueryPlacement = "inControlsMenu"
|
||||
|
||||
// Annotation event field mapping. Defines how to map a data frame field to an annotation event field.
|
||||
// +k8s:openapi-gen=true
|
||||
type DashboardAnnotationEventFieldMapping struct {
|
||||
// Source type for the field value
|
||||
Source *string `json:"source,omitempty"`
|
||||
// Constant value to use when source is "text"
|
||||
Value *string `json:"value,omitempty"`
|
||||
// Regular expression to apply to the field value
|
||||
Regex *string `json:"regex,omitempty"`
|
||||
}
|
||||
|
||||
// NewDashboardAnnotationEventFieldMapping creates a new DashboardAnnotationEventFieldMapping object.
|
||||
func NewDashboardAnnotationEventFieldMapping() *DashboardAnnotationEventFieldMapping {
|
||||
return &DashboardAnnotationEventFieldMapping{
|
||||
Source: (func(input string) *string { return &input })("field"),
|
||||
}
|
||||
}
|
||||
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
// "Crosshair" for shared crosshair.
|
||||
// "Tooltip" for shared crosshair AND shared tooltip.
|
||||
|
||||
@@ -23,6 +23,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAdHocFilterWithLabels": schema_pkg_apis_dashboard_v2beta1_DashboardAdHocFilterWithLabels(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAdhocVariableKind": schema_pkg_apis_dashboard_v2beta1_DashboardAdhocVariableKind(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAdhocVariableSpec": schema_pkg_apis_dashboard_v2beta1_DashboardAdhocVariableSpec(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationEventFieldMapping": schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationEventFieldMapping(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationPanelFilter": schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationPanelFilter(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationQueryKind": schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationQueryKind(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationQuerySpec": schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationQuerySpec(ref),
|
||||
@@ -653,6 +654,40 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardAdhocVariableSpec(ref common.Ref
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationEventFieldMapping(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Annotation event field mapping. Defines how to map a data frame field to an annotation event field.",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"source": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Source type for the field value",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"value": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Constant value to use when source is \"text\"",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"regex": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Regular expression to apply to the field value",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationPanelFilter(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
@@ -774,6 +809,21 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationQuerySpec(ref common.R
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"mappings": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Mappings define how to convert data frame fields to annotation event fields.",
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationEventFieldMapping"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"legacyOptions": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Catch-all field for datasource-specific properties. Should not be available in as code tooling.",
|
||||
@@ -794,7 +844,7 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationQuerySpec(ref common.R
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationPanelFilter", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDataQueryKind"},
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationEventFieldMapping", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationPanelFilter", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDataQueryKind"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-5
@@ -154,10 +154,22 @@
|
||||
"exclude": true
|
||||
},
|
||||
"mappings": {
|
||||
"title": "service",
|
||||
"text": "description",
|
||||
"time": "timestamp",
|
||||
"tags": "labels"
|
||||
"title": {
|
||||
"source": "field",
|
||||
"value": "service"
|
||||
},
|
||||
"text": {
|
||||
"source": "field",
|
||||
"value": "description"
|
||||
},
|
||||
"time": {
|
||||
"source": "field",
|
||||
"value": "timestamp"
|
||||
},
|
||||
"tags": {
|
||||
"source": "field",
|
||||
"value": "labels"
|
||||
}
|
||||
},
|
||||
"builtIn": 0,
|
||||
"type": "influxdb"
|
||||
@@ -214,4 +226,4 @@
|
||||
},
|
||||
"links": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v0alpha1.json
Vendored
+17
-5
@@ -151,10 +151,22 @@
|
||||
"hide": false,
|
||||
"iconColor": "#FF5722",
|
||||
"mappings": {
|
||||
"tags": "labels",
|
||||
"text": "description",
|
||||
"time": "timestamp",
|
||||
"title": "service"
|
||||
"tags": {
|
||||
"source": "field",
|
||||
"value": "labels"
|
||||
},
|
||||
"text": {
|
||||
"source": "field",
|
||||
"value": "description"
|
||||
},
|
||||
"time": {
|
||||
"source": "field",
|
||||
"value": "timestamp"
|
||||
},
|
||||
"title": {
|
||||
"source": "field",
|
||||
"value": "service"
|
||||
}
|
||||
},
|
||||
"name": "Complex Filter Annotation",
|
||||
"target": {
|
||||
@@ -227,4 +239,4 @@
|
||||
"storedVersion": "v1beta1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2alpha1.json
Vendored
+18
-6
@@ -215,13 +215,25 @@
|
||||
3
|
||||
]
|
||||
},
|
||||
"legacyOptions": {
|
||||
"mappings": {
|
||||
"tags": "labels",
|
||||
"text": "description",
|
||||
"time": "timestamp",
|
||||
"title": "service"
|
||||
"mappings": {
|
||||
"tags": {
|
||||
"source": "field",
|
||||
"value": "labels"
|
||||
},
|
||||
"text": {
|
||||
"source": "field",
|
||||
"value": "description"
|
||||
},
|
||||
"time": {
|
||||
"source": "field",
|
||||
"value": "timestamp"
|
||||
},
|
||||
"title": {
|
||||
"source": "field",
|
||||
"value": "service"
|
||||
}
|
||||
},
|
||||
"legacyOptions": {
|
||||
"type": "influxdb"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+18
-6
@@ -224,13 +224,25 @@
|
||||
3
|
||||
]
|
||||
},
|
||||
"legacyOptions": {
|
||||
"mappings": {
|
||||
"tags": "labels",
|
||||
"text": "description",
|
||||
"time": "timestamp",
|
||||
"title": "service"
|
||||
"mappings": {
|
||||
"tags": {
|
||||
"source": "field",
|
||||
"value": "labels"
|
||||
},
|
||||
"text": {
|
||||
"source": "field",
|
||||
"value": "description"
|
||||
},
|
||||
"time": {
|
||||
"source": "field",
|
||||
"value": "timestamp"
|
||||
},
|
||||
"title": {
|
||||
"source": "field",
|
||||
"value": "service"
|
||||
}
|
||||
},
|
||||
"legacyOptions": {
|
||||
"type": "influxdb"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1784,6 +1784,12 @@ func buildAnnotationQuery(annotationMap map[string]interface{}) (dashv2alpha1.Da
|
||||
filter = buildAnnotationFilter(filterMap)
|
||||
}
|
||||
|
||||
// Transform mappings
|
||||
var mappings map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping
|
||||
if mappingsMap, ok := annotationMap["mappings"].(map[string]interface{}); ok && mappingsMap != nil {
|
||||
mappings = convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
}
|
||||
|
||||
// Transform builtIn from float64 to bool
|
||||
var builtInPtr *bool
|
||||
if builtInVal, ok := annotationMap["builtIn"]; ok && builtInVal != nil {
|
||||
@@ -1809,6 +1815,7 @@ func buildAnnotationQuery(annotationMap map[string]interface{}) (dashv2alpha1.Da
|
||||
IconColor: schemaversion.GetStringValue(annotationMap, "iconColor", defaultAnnotationQuerySpec.IconColor),
|
||||
BuiltIn: builtInPtr,
|
||||
Filter: filter,
|
||||
Mappings: mappings,
|
||||
}
|
||||
|
||||
// Handle any additional properties in LegacyOptions
|
||||
@@ -1820,7 +1827,7 @@ func buildAnnotationQuery(annotationMap map[string]interface{}) (dashv2alpha1.Da
|
||||
// Add other legacy fields if they exist
|
||||
for key, value := range annotationMap {
|
||||
switch key {
|
||||
case "name", "datasource", "enable", "hide", "iconColor", "filter", "target", "builtIn", "type":
|
||||
case "name", "datasource", "enable", "hide", "iconColor", "filter", "target", "builtIn", "type", "mappings":
|
||||
// Skip already handled fields
|
||||
default:
|
||||
legacyOptions[key] = value
|
||||
@@ -1866,6 +1873,52 @@ func buildAnnotationFilter(filterMap map[string]interface{}) *dashv2alpha1.Dashb
|
||||
return filter
|
||||
}
|
||||
|
||||
func convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap map[string]interface{}) map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping {
|
||||
mappings := make(map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping)
|
||||
|
||||
for key, value := range mappingsMap {
|
||||
mapping := dashv2alpha1.DashboardAnnotationEventFieldMapping{}
|
||||
|
||||
// Handle simple string format (v1beta1 legacy format: "fieldName": "targetFieldName")
|
||||
if valueStr, ok := value.(string); ok && valueStr != "" {
|
||||
// Simple string mapping: treat as field source with the value as the field name
|
||||
defaultSource := "field"
|
||||
mapping.Source = &defaultSource
|
||||
mapping.Value = &valueStr
|
||||
mappings[key] = mapping
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle object format (v2alpha1 format: "fieldName": {"source": "field", "value": "...", "regex": "..."})
|
||||
mappingMap, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract source (defaults to "field" if not specified)
|
||||
if source, ok := mappingMap["source"].(string); ok && source != "" {
|
||||
mapping.Source = &source
|
||||
} else {
|
||||
defaultSource := "field"
|
||||
mapping.Source = &defaultSource
|
||||
}
|
||||
|
||||
// Extract value (optional)
|
||||
if valueStr, ok := mappingMap["value"].(string); ok && valueStr != "" {
|
||||
mapping.Value = &valueStr
|
||||
}
|
||||
|
||||
// Extract regex (optional)
|
||||
if regex, ok := mappingMap["regex"].(string); ok && regex != "" {
|
||||
mapping.Regex = ®ex
|
||||
}
|
||||
|
||||
mappings[key] = mapping
|
||||
}
|
||||
|
||||
return mappings
|
||||
}
|
||||
|
||||
// Panel helper functions
|
||||
|
||||
func transformPanelQueries(ctx context.Context, panelMap map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) []dashv2alpha1.DashboardPanelQueryKind {
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
package conversion
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConvertAnnotationMappings_V1beta1_to_V2alpha1(t *testing.T) {
|
||||
t.Run("should convert mappings with all fields", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "service",
|
||||
"regex": "",
|
||||
},
|
||||
"text": map[string]interface{}{
|
||||
"source": "text",
|
||||
"value": "constant text",
|
||||
},
|
||||
"time": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "timestamp",
|
||||
},
|
||||
"tags": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "labels",
|
||||
"regex": "/(.*)/",
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
require.Len(t, result, 4)
|
||||
|
||||
// Check title mapping
|
||||
titleMapping, ok := result["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
assert.Equal(t, "service", *titleMapping.Value)
|
||||
assert.Nil(t, titleMapping.Regex)
|
||||
|
||||
// Check text mapping
|
||||
textMapping, ok := result["text"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "text", *textMapping.Source)
|
||||
assert.Equal(t, "constant text", *textMapping.Value)
|
||||
assert.Nil(t, textMapping.Regex)
|
||||
|
||||
// Check time mapping
|
||||
timeMapping, ok := result["time"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *timeMapping.Source)
|
||||
assert.Equal(t, "timestamp", *timeMapping.Value)
|
||||
assert.Nil(t, timeMapping.Regex)
|
||||
|
||||
// Check tags mapping
|
||||
tagsMapping, ok := result["tags"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *tagsMapping.Source)
|
||||
assert.Equal(t, "labels", *tagsMapping.Value)
|
||||
assert.Equal(t, "/(.*)/", *tagsMapping.Regex)
|
||||
})
|
||||
|
||||
t.Run("should default source to field when not specified", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"value": "service",
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
titleMapping, ok := result["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
assert.Equal(t, "service", *titleMapping.Value)
|
||||
})
|
||||
|
||||
t.Run("should handle empty source string by defaulting to field", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "",
|
||||
"value": "service",
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
titleMapping, ok := result["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
})
|
||||
|
||||
t.Run("should handle skip source", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "skip",
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
titleMapping, ok := result["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "skip", *titleMapping.Source)
|
||||
assert.Nil(t, titleMapping.Value)
|
||||
assert.Nil(t, titleMapping.Regex)
|
||||
})
|
||||
|
||||
t.Run("should skip invalid mapping entries", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "service",
|
||||
},
|
||||
"invalid": 123, // Invalid: not a string or map
|
||||
"text": map[string]interface{}{
|
||||
"source": "text",
|
||||
"value": "constant",
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
// Should have 2 valid mappings (title and text)
|
||||
// String values are now treated as valid legacy format mappings
|
||||
require.Len(t, result, 2)
|
||||
_, ok := result["title"]
|
||||
require.True(t, ok)
|
||||
_, ok = result["text"]
|
||||
require.True(t, ok)
|
||||
_, ok = result["invalid"]
|
||||
assert.False(t, ok, "invalid entry should be skipped")
|
||||
})
|
||||
|
||||
t.Run("should handle empty mappings map", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildAnnotationQuery_Mappings(t *testing.T) {
|
||||
t.Run("should extract mappings to top-level property", func(t *testing.T) {
|
||||
annotationMap := map[string]interface{}{
|
||||
"name": "Test Annotation",
|
||||
"enable": true,
|
||||
"hide": false,
|
||||
"iconColor": "red",
|
||||
"datasource": map[string]interface{}{
|
||||
"type": "prometheus",
|
||||
"uid": "test-uid",
|
||||
},
|
||||
"target": map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
"mappings": map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "service",
|
||||
},
|
||||
"text": map[string]interface{}{
|
||||
"source": "text",
|
||||
"value": "constant text",
|
||||
},
|
||||
"time": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "timestamp",
|
||||
"regex": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := buildAnnotationQuery(annotationMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify mappings are in the correct location
|
||||
require.NotNil(t, result.Spec.Mappings)
|
||||
assert.Len(t, result.Spec.Mappings, 3)
|
||||
|
||||
// Verify mappings content
|
||||
titleMapping, ok := result.Spec.Mappings["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
assert.Equal(t, "service", *titleMapping.Value)
|
||||
|
||||
textMapping, ok := result.Spec.Mappings["text"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "text", *textMapping.Source)
|
||||
assert.Equal(t, "constant text", *textMapping.Value)
|
||||
|
||||
// Verify mappings are NOT in legacyOptions
|
||||
if result.Spec.LegacyOptions != nil {
|
||||
_, hasMappingsInLegacy := result.Spec.LegacyOptions["mappings"]
|
||||
assert.False(t, hasMappingsInLegacy, "mappings should not be in legacyOptions")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should handle annotation without mappings", func(t *testing.T) {
|
||||
annotationMap := map[string]interface{}{
|
||||
"name": "Test Annotation",
|
||||
"enable": true,
|
||||
"hide": false,
|
||||
"iconColor": "red",
|
||||
"datasource": map[string]interface{}{
|
||||
"type": "prometheus",
|
||||
"uid": "test-uid",
|
||||
},
|
||||
"target": map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := buildAnnotationQuery(annotationMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Mappings should be nil when not present
|
||||
assert.Nil(t, result.Spec.Mappings)
|
||||
})
|
||||
|
||||
t.Run("should handle empty mappings", func(t *testing.T) {
|
||||
annotationMap := map[string]interface{}{
|
||||
"name": "Test Annotation",
|
||||
"enable": true,
|
||||
"hide": false,
|
||||
"iconColor": "red",
|
||||
"datasource": map[string]interface{}{
|
||||
"type": "prometheus",
|
||||
"uid": "test-uid",
|
||||
},
|
||||
"target": map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
"mappings": map[string]interface{}{},
|
||||
}
|
||||
|
||||
result, err := buildAnnotationQuery(annotationMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Empty mappings should result in empty map
|
||||
assert.NotNil(t, result.Spec.Mappings)
|
||||
assert.Empty(t, result.Spec.Mappings)
|
||||
})
|
||||
|
||||
t.Run("should exclude mappings from legacyOptions", func(t *testing.T) {
|
||||
annotationMap := map[string]interface{}{
|
||||
"name": "Test Annotation",
|
||||
"enable": true,
|
||||
"hide": false,
|
||||
"iconColor": "red",
|
||||
"datasource": map[string]interface{}{
|
||||
"type": "prometheus",
|
||||
"uid": "test-uid",
|
||||
},
|
||||
"target": map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
"mappings": map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "service",
|
||||
},
|
||||
},
|
||||
"type": "prometheus",
|
||||
"customField": "customValue",
|
||||
}
|
||||
|
||||
result, err := buildAnnotationQuery(annotationMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify mappings are in the correct location
|
||||
require.NotNil(t, result.Spec.Mappings)
|
||||
assert.Len(t, result.Spec.Mappings, 1)
|
||||
|
||||
// Verify other fields are in legacyOptions
|
||||
require.NotNil(t, result.Spec.LegacyOptions)
|
||||
assert.Equal(t, "prometheus", result.Spec.LegacyOptions["type"])
|
||||
assert.Equal(t, "customValue", result.Spec.LegacyOptions["customField"])
|
||||
|
||||
// Verify mappings are NOT in legacyOptions
|
||||
_, hasMappingsInLegacy := result.Spec.LegacyOptions["mappings"]
|
||||
assert.False(t, hasMappingsInLegacy, "mappings should not be in legacyOptions")
|
||||
})
|
||||
|
||||
t.Run("should handle mappings with regex", func(t *testing.T) {
|
||||
annotationMap := map[string]interface{}{
|
||||
"name": "Test Annotation",
|
||||
"enable": true,
|
||||
"hide": false,
|
||||
"iconColor": "red",
|
||||
"datasource": map[string]interface{}{
|
||||
"type": "prometheus",
|
||||
"uid": "test-uid",
|
||||
},
|
||||
"target": map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
"mappings": map[string]interface{}{
|
||||
"tags": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "labels",
|
||||
"regex": "/(.*)/",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := buildAnnotationQuery(annotationMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, result.Spec.Mappings)
|
||||
tagsMapping, ok := result.Spec.Mappings["tags"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *tagsMapping.Source)
|
||||
assert.Equal(t, "labels", *tagsMapping.Value)
|
||||
assert.Equal(t, "/(.*)/", *tagsMapping.Regex)
|
||||
})
|
||||
}
|
||||
@@ -1721,6 +1721,14 @@ func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryK
|
||||
}
|
||||
}
|
||||
|
||||
// Convert mappings from v2alpha1 format back to v1beta1 format
|
||||
if len(annotation.Spec.Mappings) > 0 {
|
||||
mappings := convertAnnotationMappings_V2alpha1_to_V1beta1(annotation.Spec.Mappings)
|
||||
if len(mappings) > 0 {
|
||||
annotationMap["mappings"] = mappings
|
||||
}
|
||||
}
|
||||
|
||||
// Copy legacy options
|
||||
// This is used to copy any unknown properties from the v1 at the root of the annotations that were not handled by the conversion.
|
||||
// When they are converted into V2 they are moved to legacyOptions. Now we move them back to the root of the annotation.
|
||||
@@ -1728,7 +1736,7 @@ func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryK
|
||||
for k, v := range annotation.Spec.LegacyOptions {
|
||||
// Skip fields already handled
|
||||
if k != "name" && k != "enable" && k != "hide" && k != "iconColor" &&
|
||||
k != "datasource" && k != "target" && k != "filter" && k != "builtIn" && k != "placement" {
|
||||
k != "datasource" && k != "target" && k != "filter" && k != "builtIn" && k != "placement" && k != "mappings" {
|
||||
annotationMap[k] = v
|
||||
}
|
||||
}
|
||||
@@ -1740,6 +1748,46 @@ func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryK
|
||||
return result
|
||||
}
|
||||
|
||||
// convertAnnotationMappings_V2alpha1_to_V1beta1 converts mappings from v2alpha1 structured format
|
||||
// back to v1beta1 format. v1beta1 supports both simple string format and structured format with source/value/regex.
|
||||
// v2alpha1 format: map[string]DashboardAnnotationEventFieldMapping with Source, Value, Regex
|
||||
// v1beta1 format: map[string]interface{} where values can be either:
|
||||
// - string (legacy simple format: "fieldName": "targetFieldName")
|
||||
// - object (structured format: "fieldName": {"source": "field", "value": "...", "regex": "..."})
|
||||
func convertAnnotationMappings_V2alpha1_to_V1beta1(mappings map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for key, mapping := range mappings {
|
||||
// Always convert to structured format with source and value fields
|
||||
mappingMap := make(map[string]interface{})
|
||||
|
||||
// Source defaults to "field" if not specified
|
||||
source := "field"
|
||||
if mapping.Source != nil {
|
||||
source = *mapping.Source
|
||||
}
|
||||
mappingMap["source"] = source
|
||||
|
||||
// Value is optional (required for "field" and "text" sources, but "skip" doesn't need it)
|
||||
if mapping.Value != nil && *mapping.Value != "" {
|
||||
mappingMap["value"] = *mapping.Value
|
||||
}
|
||||
|
||||
// Regex is optional
|
||||
if mapping.Regex != nil && *mapping.Regex != "" {
|
||||
mappingMap["regex"] = *mapping.Regex
|
||||
}
|
||||
|
||||
// Include the mapping if it has source (and value for non-skip sources)
|
||||
// Skip source doesn't require a value
|
||||
if source == "skip" || (mapping.Value != nil && *mapping.Value != "") {
|
||||
result[key] = mappingMap
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Enum transformation functions (reverse of v1→v2)
|
||||
func transformVariableHideFromEnum(hide dashv2alpha1.DashboardVariableHide) interface{} {
|
||||
switch hide {
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package conversion
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
)
|
||||
|
||||
func TestConvertAnnotationMappings_V2alpha1_to_V1beta1(t *testing.T) {
|
||||
t.Run("should convert simple field mappings to structured format with source and value", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("service"),
|
||||
},
|
||||
"text": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("description"),
|
||||
},
|
||||
"time": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("timestamp"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 3)
|
||||
// All mappings should be in structured format with source and value
|
||||
titleMapping, ok := result["title"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", titleMapping["source"])
|
||||
assert.Equal(t, "service", titleMapping["value"])
|
||||
|
||||
textMapping, ok := result["text"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", textMapping["source"])
|
||||
assert.Equal(t, "description", textMapping["value"])
|
||||
|
||||
timeMapping, ok := result["time"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", timeMapping["source"])
|
||||
assert.Equal(t, "timestamp", timeMapping["value"])
|
||||
})
|
||||
|
||||
t.Run("should convert mappings with default field source to structured format", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: nil, // nil defaults to "field"
|
||||
Value: ptr.To("service"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
titleMapping, ok := result["title"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", titleMapping["source"])
|
||||
assert.Equal(t, "service", titleMapping["value"])
|
||||
})
|
||||
|
||||
t.Run("should preserve complex mappings with regex as structured format", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"tags": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("labels"),
|
||||
Regex: ptr.To("/(.*)/"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
tagsMapping, ok := result["tags"].(map[string]interface{})
|
||||
require.True(t, ok, "tags mapping should be a map")
|
||||
assert.Equal(t, "field", tagsMapping["source"])
|
||||
assert.Equal(t, "labels", tagsMapping["value"])
|
||||
assert.Equal(t, "/(.*)/", tagsMapping["regex"])
|
||||
})
|
||||
|
||||
t.Run("should preserve mappings with non-field source as structured format", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"text": {
|
||||
Source: ptr.To("text"),
|
||||
Value: ptr.To("constant text"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
textMapping, ok := result["text"].(map[string]interface{})
|
||||
require.True(t, ok, "text mapping should be a map")
|
||||
assert.Equal(t, "text", textMapping["source"])
|
||||
assert.Equal(t, "constant text", textMapping["value"])
|
||||
})
|
||||
|
||||
t.Run("should preserve mappings with skip source as structured format", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: ptr.To("skip"),
|
||||
},
|
||||
"text": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("description"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 2)
|
||||
// Skip mapping should be preserved as structured format
|
||||
titleMapping, ok := result["title"].(map[string]interface{})
|
||||
require.True(t, ok, "skip mapping should be preserved as map")
|
||||
assert.Equal(t, "skip", titleMapping["source"])
|
||||
// Field mapping should be structured format with source and value
|
||||
textMapping, ok := result["text"].(map[string]interface{})
|
||||
require.True(t, ok, "field mapping should be structured format")
|
||||
assert.Equal(t, "field", textMapping["source"])
|
||||
assert.Equal(t, "description", textMapping["value"])
|
||||
})
|
||||
|
||||
t.Run("should skip mappings without value", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: ptr.To("field"),
|
||||
Value: nil,
|
||||
},
|
||||
"text": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To(""),
|
||||
},
|
||||
"time": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("timestamp"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
_, ok := result["title"]
|
||||
assert.False(t, ok, "mapping without value should be skipped")
|
||||
_, ok = result["text"]
|
||||
assert.False(t, ok, "mapping with empty value should be skipped")
|
||||
_, ok = result["time"]
|
||||
assert.True(t, ok, "mapping with value should be included")
|
||||
})
|
||||
|
||||
t.Run("should handle empty mappings map", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("should handle all mappings in structured format", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("service"),
|
||||
},
|
||||
"tags": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("labels"),
|
||||
Regex: ptr.To("/(.*)/"),
|
||||
},
|
||||
"text": {
|
||||
Source: ptr.To("text"),
|
||||
Value: ptr.To("constant"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 3)
|
||||
// All mappings should be in structured format
|
||||
titleMapping, ok := result["title"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", titleMapping["source"])
|
||||
assert.Equal(t, "service", titleMapping["value"])
|
||||
|
||||
tagsMapping, ok := result["tags"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", tagsMapping["source"])
|
||||
assert.Equal(t, "labels", tagsMapping["value"])
|
||||
assert.Equal(t, "/(.*)/", tagsMapping["regex"])
|
||||
|
||||
textMapping, ok := result["text"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "text", textMapping["source"])
|
||||
assert.Equal(t, "constant", textMapping["value"])
|
||||
})
|
||||
}
|
||||
@@ -113,6 +113,11 @@ func convertAnnotationQuery_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardAnnota
|
||||
out.Spec.Filter = (*dashv2beta1.DashboardAnnotationPanelFilter)(in.Spec.Filter)
|
||||
out.Spec.LegacyOptions = in.Spec.LegacyOptions
|
||||
|
||||
// Convert mappings
|
||||
if in.Spec.Mappings != nil {
|
||||
out.Spec.Mappings = convertAnnotationMappings_V2alpha1_to_V2beta1(in.Spec.Mappings)
|
||||
}
|
||||
|
||||
// Convert query - move datasource from annotation spec to query
|
||||
if err := convertDataQuery_V2alpha1_to_V2beta1(in.Spec.Query, &out.Spec.Query, in.Spec.Datasource, scope); err != nil {
|
||||
return err
|
||||
@@ -982,3 +987,18 @@ func convertRowLayout_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardGridLayoutKi
|
||||
func convertTabLayout_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, out *dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, scope conversion.Scope) error {
|
||||
return convertLayout_V2alpha1_to_V2beta1(in, out, scope)
|
||||
}
|
||||
|
||||
func convertAnnotationMappings_V2alpha1_to_V2beta1(in map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping) map[string]dashv2beta1.DashboardAnnotationEventFieldMapping {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]dashv2beta1.DashboardAnnotationEventFieldMapping, len(in))
|
||||
for key, mapping := range in {
|
||||
out[key] = dashv2beta1.DashboardAnnotationEventFieldMapping{
|
||||
Source: mapping.Source,
|
||||
Value: mapping.Value,
|
||||
Regex: mapping.Regex,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -115,6 +115,83 @@ func TestV2alpha1ToV2beta1(t *testing.T) {
|
||||
assert.True(t, variable.SwitchVariableKind.Spec.SkipUrlSync)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "annotation query with mappings",
|
||||
createV2alpha1: func() *dashv2alpha1.Dashboard {
|
||||
sourceField := "field"
|
||||
sourceText := "text"
|
||||
valueService := "service"
|
||||
valueConstant := "constant text"
|
||||
regexPattern := "/(.*)/"
|
||||
return &dashv2alpha1.Dashboard{
|
||||
Spec: dashv2alpha1.DashboardSpec{
|
||||
Title: "Test Dashboard",
|
||||
Annotations: []dashv2alpha1.DashboardAnnotationQueryKind{
|
||||
{
|
||||
Kind: "AnnotationQuery",
|
||||
Spec: dashv2alpha1.DashboardAnnotationQuerySpec{
|
||||
Name: "Test Annotation",
|
||||
Enable: true,
|
||||
Hide: false,
|
||||
IconColor: "red",
|
||||
Query: &dashv2alpha1.DashboardDataQueryKind{
|
||||
Kind: "prometheus",
|
||||
Spec: map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
},
|
||||
Mappings: map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: &sourceField,
|
||||
Value: &valueService,
|
||||
},
|
||||
"text": {
|
||||
Source: &sourceText,
|
||||
Value: &valueConstant,
|
||||
},
|
||||
"tags": {
|
||||
Source: &sourceField,
|
||||
Value: &valueService,
|
||||
Regex: ®exPattern,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
validateV2beta1: func(t *testing.T, v2beta1 *dashv2beta1.Dashboard) {
|
||||
require.Len(t, v2beta1.Spec.Annotations, 1)
|
||||
annotation := v2beta1.Spec.Annotations[0]
|
||||
assert.Equal(t, "Test Annotation", annotation.Spec.Name)
|
||||
|
||||
// Verify mappings are preserved
|
||||
require.NotNil(t, annotation.Spec.Mappings)
|
||||
assert.Len(t, annotation.Spec.Mappings, 3)
|
||||
|
||||
// Check title mapping
|
||||
titleMapping, ok := annotation.Spec.Mappings["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
assert.Equal(t, "service", *titleMapping.Value)
|
||||
assert.Nil(t, titleMapping.Regex)
|
||||
|
||||
// Check text mapping
|
||||
textMapping, ok := annotation.Spec.Mappings["text"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "text", *textMapping.Source)
|
||||
assert.Equal(t, "constant text", *textMapping.Value)
|
||||
assert.Nil(t, textMapping.Regex)
|
||||
|
||||
// Check tags mapping
|
||||
tagsMapping, ok := annotation.Spec.Mappings["tags"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *tagsMapping.Source)
|
||||
assert.Equal(t, "service", *tagsMapping.Value)
|
||||
assert.Equal(t, "/(.*)/", *tagsMapping.Regex)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -114,6 +114,11 @@ func convertAnnotationQuery_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardAnnotat
|
||||
out.Spec.Filter = (*dashv2alpha1.DashboardAnnotationPanelFilter)(in.Spec.Filter)
|
||||
out.Spec.LegacyOptions = in.Spec.LegacyOptions
|
||||
|
||||
// Convert mappings
|
||||
if in.Spec.Mappings != nil {
|
||||
out.Spec.Mappings = convertAnnotationMappings_V2beta1_to_V2alpha1(in.Spec.Mappings)
|
||||
}
|
||||
|
||||
// Convert query - move datasource from query back to annotation spec
|
||||
query, datasource, err := convertDataQuery_V2beta1_to_V2alpha1(&in.Spec.Query, scope)
|
||||
if err != nil {
|
||||
@@ -1023,3 +1028,18 @@ func convertRowLayout_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardGridLayoutKin
|
||||
func convertTabLayout_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, out *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, scope conversion.Scope) error {
|
||||
return convertLayout_V2beta1_to_V2alpha1(in, out, scope)
|
||||
}
|
||||
|
||||
func convertAnnotationMappings_V2beta1_to_V2alpha1(in map[string]dashv2beta1.DashboardAnnotationEventFieldMapping) map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping, len(in))
|
||||
for key, mapping := range in {
|
||||
out[key] = dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
Source: mapping.Source,
|
||||
Value: mapping.Value,
|
||||
Regex: mapping.Regex,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -586,6 +586,85 @@ func TestV2beta1ToV2alpha1(t *testing.T) {
|
||||
assert.False(t, *row.Spec.Collapse)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "annotation query with mappings",
|
||||
createV2beta1: func() *dashv2beta1.Dashboard {
|
||||
sourceField := "field"
|
||||
sourceText := "text"
|
||||
valueService := "service"
|
||||
valueConstant := "constant text"
|
||||
regexPattern := "/(.*)/"
|
||||
return &dashv2beta1.Dashboard{
|
||||
Spec: dashv2beta1.DashboardSpec{
|
||||
Title: "Test Dashboard",
|
||||
Annotations: []dashv2beta1.DashboardAnnotationQueryKind{
|
||||
{
|
||||
Kind: "AnnotationQuery",
|
||||
Spec: dashv2beta1.DashboardAnnotationQuerySpec{
|
||||
Name: "Test Annotation",
|
||||
Enable: true,
|
||||
Hide: false,
|
||||
IconColor: "red",
|
||||
Query: dashv2beta1.DashboardDataQueryKind{
|
||||
Kind: "DataQuery",
|
||||
Group: "prometheus",
|
||||
Version: "v0",
|
||||
Spec: map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
},
|
||||
Mappings: map[string]dashv2beta1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: &sourceField,
|
||||
Value: &valueService,
|
||||
},
|
||||
"text": {
|
||||
Source: &sourceText,
|
||||
Value: &valueConstant,
|
||||
},
|
||||
"tags": {
|
||||
Source: &sourceField,
|
||||
Value: &valueService,
|
||||
Regex: ®exPattern,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) {
|
||||
require.Len(t, v2alpha1.Spec.Annotations, 1)
|
||||
annotation := v2alpha1.Spec.Annotations[0]
|
||||
assert.Equal(t, "Test Annotation", annotation.Spec.Name)
|
||||
|
||||
// Verify mappings are preserved
|
||||
require.NotNil(t, annotation.Spec.Mappings)
|
||||
assert.Len(t, annotation.Spec.Mappings, 3)
|
||||
|
||||
// Check title mapping
|
||||
titleMapping, ok := annotation.Spec.Mappings["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
assert.Equal(t, "service", *titleMapping.Value)
|
||||
assert.Nil(t, titleMapping.Regex)
|
||||
|
||||
// Check text mapping
|
||||
textMapping, ok := annotation.Spec.Mappings["text"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "text", *textMapping.Source)
|
||||
assert.Equal(t, "constant text", *textMapping.Value)
|
||||
assert.Nil(t, textMapping.Regex)
|
||||
|
||||
// Check tags mapping
|
||||
tagsMapping, ok := annotation.Spec.Mappings["tags"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *tagsMapping.Source)
|
||||
assert.Equal(t, "service", *tagsMapping.Value)
|
||||
assert.Equal(t, "/(.*)/", *tagsMapping.Regex)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface AnnotationQuerySpec {
|
||||
name: string;
|
||||
builtIn?: boolean;
|
||||
filter?: AnnotationPanelFilter;
|
||||
// Mappings define how to convert data frame fields to annotation event fields.
|
||||
mappings?: Record<string, AnnotationEventFieldMapping>;
|
||||
// Catch-all field for datasource-specific properties
|
||||
legacyOptions?: Record<string, any>;
|
||||
}
|
||||
@@ -64,6 +66,20 @@ export const defaultAnnotationPanelFilter = (): AnnotationPanelFilter => ({
|
||||
ids: [],
|
||||
});
|
||||
|
||||
// Annotation event field mapping. Defines how to map a data frame field to an annotation event field.
|
||||
export interface AnnotationEventFieldMapping {
|
||||
// Source type for the field value
|
||||
source?: string;
|
||||
// Constant value to use when source is "text"
|
||||
value?: string;
|
||||
// Regular expression to apply to the field value
|
||||
regex?: string;
|
||||
}
|
||||
|
||||
export const defaultAnnotationEventFieldMapping = (): AnnotationEventFieldMapping => ({
|
||||
source: "field",
|
||||
});
|
||||
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
// "Crosshair" for shared crosshair.
|
||||
// "Tooltip" for shared crosshair AND shared tooltip.
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface AnnotationQuerySpec {
|
||||
filter?: AnnotationPanelFilter;
|
||||
// Placement can be used to display the annotation query somewhere else on the dashboard other than the default location.
|
||||
placement?: "inControlsMenu";
|
||||
// Mappings define how to convert data frame fields to annotation event fields.
|
||||
mappings?: Record<string, AnnotationEventFieldMapping>;
|
||||
// Catch-all field for datasource-specific properties. Should not be available in as code tooling.
|
||||
legacyOptions?: Record<string, any>;
|
||||
}
|
||||
@@ -69,6 +71,20 @@ export const defaultAnnotationPanelFilter = (): AnnotationPanelFilter => ({
|
||||
// - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu
|
||||
export const AnnotationQueryPlacement = "inControlsMenu";
|
||||
|
||||
// Annotation event field mapping. Defines how to map a data frame field to an annotation event field.
|
||||
export interface AnnotationEventFieldMapping {
|
||||
// Source type for the field value
|
||||
source?: string;
|
||||
// Constant value to use when source is "text"
|
||||
value?: string;
|
||||
// Regular expression to apply to the field value
|
||||
regex?: string;
|
||||
}
|
||||
|
||||
export const defaultAnnotationEventFieldMapping = (): AnnotationEventFieldMapping => ({
|
||||
source: "field",
|
||||
});
|
||||
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
// "Crosshair" for shared crosshair.
|
||||
// "Tooltip" for shared crosshair AND shared tooltip.
|
||||
|
||||
@@ -1275,6 +1275,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationEventFieldMapping": {
|
||||
"description": "Annotation event field mapping. Defines how to map a data frame field to an annotation event field.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"regex": {
|
||||
"description": "Regular expression to apply to the field value",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"description": "Source type for the field value",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"description": "Constant value to use when source is \"text\"",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationPanelFilter": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -1354,6 +1372,18 @@
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"mappings": {
|
||||
"description": "Mappings define how to convert data frame fields to annotation event fields.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"default": {},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationEventFieldMapping"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AnnotationQuery } from '@grafana/data';
|
||||
import { AnnotationQuery, AnnotationEventFieldSource } from '@grafana/data';
|
||||
import { AnnotationQueryKind } from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
|
||||
import { transformV1ToV2AnnotationQuery, transformV2ToV1AnnotationQuery } from './annotations';
|
||||
@@ -220,4 +220,145 @@ describe('V1<->V2 annotation convertions', () => {
|
||||
const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2);
|
||||
expect(resultV1).toEqual(annotationDefinition);
|
||||
});
|
||||
|
||||
test('given annotations with mappings', () => {
|
||||
const annotationDefinition: AnnotationQuery = {
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'uid',
|
||||
},
|
||||
enable: true,
|
||||
hide: false,
|
||||
iconColor: 'red',
|
||||
name: 'prom-annos',
|
||||
target: {
|
||||
// @ts-expect-error
|
||||
expr: '{action="add_client"}',
|
||||
refId: 'Anno',
|
||||
},
|
||||
mappings: {
|
||||
title: {
|
||||
source: AnnotationEventFieldSource.Field,
|
||||
value: 'service',
|
||||
},
|
||||
text: {
|
||||
source: AnnotationEventFieldSource.Text,
|
||||
value: 'constant text',
|
||||
},
|
||||
tags: {
|
||||
source: AnnotationEventFieldSource.Field,
|
||||
value: 'labels',
|
||||
regex: '/(.*)/',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const expectedV2: AnnotationQueryKind = {
|
||||
kind: 'AnnotationQuery',
|
||||
spec: {
|
||||
enable: true,
|
||||
hide: false,
|
||||
iconColor: 'red',
|
||||
name: 'prom-annos',
|
||||
query: {
|
||||
kind: 'DataQuery',
|
||||
group: 'prometheus',
|
||||
version: 'v0',
|
||||
datasource: {
|
||||
name: 'uid',
|
||||
},
|
||||
spec: {
|
||||
expr: '{action="add_client"}',
|
||||
refId: 'Anno',
|
||||
},
|
||||
},
|
||||
mappings: {
|
||||
title: {
|
||||
source: 'field',
|
||||
value: 'service',
|
||||
},
|
||||
text: {
|
||||
source: 'text',
|
||||
value: 'constant text',
|
||||
},
|
||||
tags: {
|
||||
source: 'field',
|
||||
value: 'labels',
|
||||
regex: '/(.*)/',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const resultV2: AnnotationQueryKind = transformV1ToV2AnnotationQuery(annotationDefinition, 'prometheus', 'uid');
|
||||
expect(resultV2).toEqual(expectedV2);
|
||||
|
||||
const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2);
|
||||
expect(resultV1).toEqual(annotationDefinition);
|
||||
});
|
||||
|
||||
test('given annotations with mappings and legacyOptions', () => {
|
||||
const annotationDefinition: AnnotationQuery = {
|
||||
datasource: {
|
||||
type: 'elasticsearch',
|
||||
uid: 'uid',
|
||||
},
|
||||
enable: true,
|
||||
hide: false,
|
||||
iconColor: 'red',
|
||||
name: 'elastic-annos',
|
||||
target: {
|
||||
// @ts-expect-error
|
||||
query: 'test query',
|
||||
refId: 'Anno',
|
||||
},
|
||||
mappings: {
|
||||
title: {
|
||||
source: AnnotationEventFieldSource.Field,
|
||||
value: 'service',
|
||||
},
|
||||
},
|
||||
// These should go to legacyOptions
|
||||
tagsField: 'asd',
|
||||
textField: 'asd',
|
||||
};
|
||||
|
||||
const expectedV2: AnnotationQueryKind = {
|
||||
kind: 'AnnotationQuery',
|
||||
spec: {
|
||||
enable: true,
|
||||
hide: false,
|
||||
iconColor: 'red',
|
||||
name: 'elastic-annos',
|
||||
query: {
|
||||
kind: 'DataQuery',
|
||||
group: 'elasticsearch',
|
||||
version: 'v0',
|
||||
datasource: {
|
||||
name: 'uid',
|
||||
},
|
||||
spec: {
|
||||
query: 'test query',
|
||||
refId: 'Anno',
|
||||
},
|
||||
},
|
||||
mappings: {
|
||||
title: {
|
||||
source: 'field',
|
||||
value: 'service',
|
||||
},
|
||||
},
|
||||
legacyOptions: {
|
||||
tagsField: 'asd',
|
||||
textField: 'asd',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const resultV2: AnnotationQueryKind = transformV1ToV2AnnotationQuery(annotationDefinition, 'elasticsearch', 'uid');
|
||||
expect(resultV2).toEqual(expectedV2);
|
||||
|
||||
const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2);
|
||||
expect(resultV1).toEqual(annotationDefinition);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,7 +74,10 @@ export function transformV1ToV2AnnotationQuery(
|
||||
result.spec.filter = annotation.filter;
|
||||
}
|
||||
|
||||
// TODO: add mappings
|
||||
// Add mappings if they exist
|
||||
if (annotation.mappings && Object.keys(annotation.mappings).length > 0) {
|
||||
result.spec.mappings = annotation.mappings;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -88,9 +91,13 @@ export function transformV2ToV1AnnotationQuery(annotation: AnnotationQueryKind):
|
||||
hide: annotation.spec.hide,
|
||||
iconColor: annotation.spec.iconColor,
|
||||
name: annotation.spec.name,
|
||||
// TOOO: mappings
|
||||
};
|
||||
|
||||
// Add mappings if they exist
|
||||
if (annotation.spec.mappings && Object.keys(annotation.spec.mappings).length > 0) {
|
||||
annoQuerySpec.mappings = annotation.spec.mappings;
|
||||
}
|
||||
|
||||
// Add placement if it exists
|
||||
if (annotation.spec.placement) {
|
||||
annoQuerySpec.placement = annotation.spec.placement;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { MetricFindValue, TypedVariableModel } from '@grafana/data';
|
||||
import { MetricFindValue, TypedVariableModel, AnnotationQuery } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
AnnotationQuery,
|
||||
DataQuery,
|
||||
DataSourceRef,
|
||||
Panel,
|
||||
@@ -845,7 +844,7 @@ function getVariables(vars: TypedVariableModel[]): DashboardV2Spec['variables']
|
||||
function getAnnotations(annotations: AnnotationQuery[]): DashboardV2Spec['annotations'] {
|
||||
return annotations.map((a) => {
|
||||
// Extract properties that are explicitly handled
|
||||
const { name, enable, hide, iconColor, builtIn, datasource, target, filter, ...legacyOptions } = a;
|
||||
const { name, enable, hide, iconColor, builtIn, datasource, target, filter, mappings, ...legacyOptions } = a;
|
||||
|
||||
const aq: AnnotationQueryKind = {
|
||||
kind: 'AnnotationQuery',
|
||||
@@ -855,6 +854,7 @@ function getAnnotations(annotations: AnnotationQuery[]): DashboardV2Spec['annota
|
||||
hide: Boolean(hide),
|
||||
iconColor: iconColor,
|
||||
builtIn: Boolean(builtIn),
|
||||
...(mappings && { mappings: transformAnnotationMappingsV1ToV2(mappings) }),
|
||||
query: {
|
||||
kind: 'DataQuery',
|
||||
version: defaultDataQueryKind().version,
|
||||
@@ -1349,3 +1349,25 @@ export function transformDashboardV2SpecToV1(spec: DashboardV2Spec, metadata: Ob
|
||||
templating: { list: variables },
|
||||
};
|
||||
}
|
||||
|
||||
export function transformAnnotationMappingsV1ToV2(
|
||||
mappings: AnnotationQuery['mappings']
|
||||
): AnnotationQueryKind['spec']['mappings'] {
|
||||
if (!mappings) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(mappings).map(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
return [key, { source: 'field', value }];
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return [key, value.source ? value : { source: 'field', ...value }];
|
||||
}
|
||||
|
||||
return [key, value];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user