diff --git a/Dockerfile b/Dockerfile index 5bcca3643b5..d3f63dd0544 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ ARG JS_SRC=js-builder # By using FROM instructions we can delegate dependency updates to dependabot FROM alpine:3.22.2 AS alpine-base FROM ubuntu:22.04 AS ubuntu-base -FROM golang:1.25.3-alpine AS go-builder-base +FROM golang:1.25.5-alpine AS go-builder-base FROM --platform=${JS_PLATFORM} node:24-alpine AS js-builder-base # Javascript build stage FROM --platform=${JS_PLATFORM} ${JS_IMAGE} AS js-builder diff --git a/Makefile b/Makefile index 56c2a2c63ff..a5353e95567 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ WIRE_TAGS = "oss" include .citools/Variables.mk GO = go -GO_VERSION = 1.25.3 +GO_VERSION = 1.25.5 GO_LINT_FILES ?= $(shell ./scripts/go-workspace/golangci-lint-includes.sh) GO_TEST_FILES ?= $(shell ./scripts/go-workspace/test-includes.sh) SH_FILES ?= $(shell find ./scripts -name *.sh) diff --git a/apps/alerting/historian/pkg/app/app.go b/apps/alerting/historian/pkg/app/app.go index e34e03c9044..2996a21a231 100644 --- a/apps/alerting/historian/pkg/app/app.go +++ b/apps/alerting/historian/pkg/app/app.go @@ -16,6 +16,12 @@ import ( func New(cfg app.Config) (app.App, error) { runtimeConfig := cfg.SpecificConfig.(config.RuntimeConfig) + alertStateHandler := runtimeConfig.GetAlertStateHistoryHandler + if alertStateHandler == nil { + alertStateHandler = NewErrorHandler("no alert state handler") + } + notificationHandler := NewErrorHandler("unimplemented") + simpleConfig := simple.AppConfig{ Name: "alerting.historian", KubeConfig: cfg.KubeConfig, @@ -25,12 +31,12 @@ func New(cfg app.Config) (app.App, error) { Namespaced: true, Path: "/alertstate/history", Method: "GET", - }: runtimeConfig.GetAlertStateHistoryHandler, + }: alertStateHandler, { Namespaced: true, Path: "/notification/query", Method: "POST", - }: UnimplementedHandler, + }: notificationHandler, }, }, // TODO: Remove when SDK is fixed. @@ -54,12 +60,14 @@ func New(cfg app.Config) (app.App, error) { return a, nil } -func UnimplementedHandler(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error { - return &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Status: metav1.StatusFailure, - Code: http.StatusUnprocessableEntity, - Message: "unimplemented", - }, +func NewErrorHandler(message string) simple.AppCustomRouteHandler { + return func(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error { + return &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusUnprocessableEntity, + Message: message, + }, + } } } diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index d0d5f68e72d..3fe93d7305f 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -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: { diff --git a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue index b81175db77d..ef4a27fd6b7 100644 --- a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue @@ -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: { diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue index 5abb23ba7e5..d822ad9f38a 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @@ -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: { diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go index 29e207e2927..625c6fe17c0 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -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. diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go index 98da1506cd3..ff7429d7c2b 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go @@ -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"}, } } diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue index b2340d5c22e..0c061075e6d 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue @@ -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: { diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go index ffdad0045c6..a6e63aa0bcc 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go @@ -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. diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go index f833f1ddee0..3a129374192 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go @@ -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"}, } } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.annotation-conversions.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.annotation-conversions.json index 3b2b705cd5b..581d7109346 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.annotation-conversions.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.annotation-conversions.json @@ -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": [] } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v0alpha1.json index 1f0c641c7a6..0e488407f34 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v0alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v0alpha1.json @@ -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" } } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2alpha1.json index 2430b59063b..387ba145494 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2alpha1.json @@ -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" } } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2beta1.json index 3f0eb4d3eb4..a4dde4d26ac 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2beta1.json @@ -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" } } diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go index 9efd0b68f23..da8e8a6ae28 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go @@ -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 { diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_mappings_test.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_mappings_test.go new file mode 100644 index 00000000000..891fbafd668 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_mappings_test.go @@ -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) + }) +} diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go index cfe022b1c91..180a8d603bb 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go @@ -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 { diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1_mappings_test.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1_mappings_test.go new file mode 100644 index 00000000000..4f7fac69959 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1_mappings_test.go @@ -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"]) + }) +} diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go index 3853a420165..452f0140047 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go @@ -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 +} diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go index 3451234cdec..028197bd655 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go @@ -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 { diff --git a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go index 1feea41d8f5..95dfcf76c9d 100644 --- a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go @@ -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 +} diff --git a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go index 25703b56aa7..23dcd36ee17 100644 --- a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go @@ -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 { diff --git a/apps/iam/local/Dockerfile b/apps/iam/local/Dockerfile index 4777b0a466d..c6db8475d03 100644 --- a/apps/iam/local/Dockerfile +++ b/apps/iam/local/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM golang:1.25.3-alpine AS builder +FROM golang:1.25.5-alpine AS builder # Set working directory WORKDIR /app diff --git a/devenv/docker/blocks/stateful_webhook/Dockerfile b/devenv/docker/blocks/stateful_webhook/Dockerfile index f4eaaa14256..a9eaa77b382 100644 --- a/devenv/docker/blocks/stateful_webhook/Dockerfile +++ b/devenv/docker/blocks/stateful_webhook/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.25.3 +FROM golang:1.25.5 ADD main.go /go/src/webhook/main.go diff --git a/docs/sources/visualizations/dashboards/manage-dashboards/index.md b/docs/sources/visualizations/dashboards/manage-dashboards/index.md index 268b7a7ad8e..f42892d4f39 100644 --- a/docs/sources/visualizations/dashboards/manage-dashboards/index.md +++ b/docs/sources/visualizations/dashboards/manage-dashboards/index.md @@ -133,6 +133,10 @@ After this limit is reached, the oldest deleted dashboards are permanently remov You can access the list of deleted dashboards from the **Dashboards** page by clicking the **Recently deleted** button, or by navigating to **Dashboards > Recently deleted**. +{{% admonition type="note" %}} +Only users with admin rights can access the **Restore dashboards** page. +{{% /admonition %}} + To restore one or more dashboards, follow these steps: 1. In the main menu, click **Dashboards > Recently deleted** or click the **Recently deleted** button from the **Dashboards** page. @@ -144,9 +148,13 @@ To restore one or more dashboards, follow these steps: - If the original folder no longer exists, you’ll need to select a new target folder. 4. Click **Restore**. -{{% admonition type="note" %}} -Only users with admin rights can access the **Restore dashboards** page. -{{% /admonition %}} +### Limitations + +Restoring dashboards has the following limitations: + +- **Permissions aren't preserved** - Dashboard-specific permissions are not restored. After restoration, you'll need to manually reconfigure any custom permissions that were previously set on the dashboard. +- **Folder-level permissions apply** - Restored dashboards inherit the permissions of the target folder you select during restoration. +- **Version history is reset** - The dashboard's version history is not preserved. After restoration, the dashboard starts with version 1, and all previous versions are lost. ## Set up generative AI features for dashboards diff --git a/go.work.sum b/go.work.sum index 2e139bbf154..aad28b17bf8 100644 --- a/go.work.sum +++ b/go.work.sum @@ -770,6 +770,7 @@ github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5 github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts index 8c7d323a544..67d24915bf1 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts @@ -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; // Catch-all field for datasource-specific properties legacyOptions?: Record; } @@ -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. diff --git a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts index 95c0dac4230..315fa4768a4 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts @@ -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; // Catch-all field for datasource-specific properties. Should not be available in as code tooling. legacyOptions?: Record; } @@ -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. diff --git a/pkg/registry/apps/alerting/historian/register.go b/pkg/registry/apps/alerting/historian/register.go index fb0e90e7062..78312c326aa 100644 --- a/pkg/registry/apps/alerting/historian/register.go +++ b/pkg/registry/apps/alerting/historian/register.go @@ -26,19 +26,21 @@ func RegisterAppInstaller( cfg *setting.Cfg, ng *ngalert.AlertNG, ) (*AlertingHistorianAppInstaller, error) { - if ng.IsDisabled() { - log.New("app-registry").Info("Skipping Kubernetes Alerting Historian apiserver (historian.alerting.grafana.app): Unified Alerting is disabled") - return nil, nil - } - installer := &AlertingHistorianAppInstaller{} + appSpecificConfig := historianAppConfig.RuntimeConfig{} - handlers := &handlers{ - historian: ng.Api.Historian, - } + // If we're provided an AlertNG, then call back into that for things we need. + // This is a temporary whilst building out the app; we should not depend on it. + if ng != nil { + if ng.IsDisabled() { + log.New("app-registry").Info("Skipping Kubernetes Alerting Historian apiserver (historian.alerting.grafana.app): Unified Alerting is disabled") + return nil, nil + } - appSpecificConfig := historianAppConfig.RuntimeConfig{ - GetAlertStateHistoryHandler: handlers.GetAlertStateHistoryHandler, + handlers := &handlers{ + historian: ng.Api.Historian, + } + appSpecificConfig.GetAlertStateHistoryHandler = handlers.GetAlertStateHistoryHandler } provider := simple.NewAppProvider(apis.LocalManifest(), appSpecificConfig, historianApp.New) diff --git a/pkg/services/ngalert/eval/eval.go b/pkg/services/ngalert/eval/eval.go index 67aa263b24a..687e9b2dd03 100644 --- a/pkg/services/ngalert/eval/eval.go +++ b/pkg/services/ngalert/eval/eval.go @@ -437,6 +437,7 @@ type NumberValueCapture struct { Var string // RefID IsDatasourceNode bool Labels data.Labels + Type string // Expression type (reduce, threshold, classic_conditions, etc.) Value *float64 } @@ -462,17 +463,33 @@ func IsNoData(res backend.DataResponse) bool { func queryDataResponseToExecutionResults(c models.Condition, execResp *backend.QueryDataResponse) ExecutionResults { // captures contains the values of all instant queries and expressions for each dimension captures := make(map[string]map[data.Fingerprint]NumberValueCapture) + + // Build a lookup table for expression types by RefID + expressionTypes := make(map[string]string) + for _, query := range c.Data { + if exprType, err := query.GetExpressionType(); err == nil { + expressionTypes[query.RefID] = exprType + } + } + captureFn := func(refID string, datasourceType expr.NodeType, labels data.Labels, value *float64) { m := captures[refID] if m == nil { m = make(map[data.Fingerprint]NumberValueCapture) } fp := labels.Fingerprint() + + exprType := expressionTypes[refID] + if exprType == "" && datasourceType == expr.TypeDatasourceNode { + exprType = "query" + } + m[fp] = NumberValueCapture{ Var: refID, IsDatasourceNode: datasourceType == expr.TypeDatasourceNode, Value: value, Labels: labels.Copy(), + Type: exprType, } captures[refID] = m } diff --git a/pkg/services/ngalert/eval/extract_md.go b/pkg/services/ngalert/eval/extract_md.go index f1a1dbe4f0c..470362d15fa 100644 --- a/pkg/services/ngalert/eval/extract_md.go +++ b/pkg/services/ngalert/eval/extract_md.go @@ -27,6 +27,7 @@ func extractEvalString(frame *data.Frame) (s string) { sb.WriteString(fmt.Sprintf("var='%s%v' ", frame.RefID, i)) sb.WriteString(fmt.Sprintf("metric='%s' ", m.Metric)) sb.WriteString(fmt.Sprintf("labels={%s} ", m.Labels)) + sb.WriteString("type='classic_conditions' ") valString := "null" if m.Value != nil { @@ -53,6 +54,9 @@ func extractEvalString(frame *data.Frame) (s string) { sb.WriteString("[ ") sb.WriteString(fmt.Sprintf("var='%s' ", capture.Var)) sb.WriteString(fmt.Sprintf("labels={%s} ", capture.Labels)) + if capture.Type != "" { + sb.WriteString(fmt.Sprintf("type='%s' ", capture.Type)) + } valString := "null" if capture.Value != nil { valString = fmt.Sprintf("%v", *capture.Value) @@ -92,6 +96,7 @@ func extractValues(frame *data.Frame) map[string]NumberValueCapture { Var: frame.RefID, Labels: match.Labels, Value: match.Value, + Type: "classic_conditions", } } return v diff --git a/pkg/services/ngalert/eval/extract_md_test.go b/pkg/services/ngalert/eval/extract_md_test.go index ad8c7ff5b23..49c8704767a 100644 --- a/pkg/services/ngalert/eval/extract_md_test.go +++ b/pkg/services/ngalert/eval/extract_md_test.go @@ -21,7 +21,7 @@ func TestExtractEvalString(t *testing.T) { inFrame: newMetaFrame([]classic.EvalMatch{ {Metric: "Test", Labels: data.Labels{"host": "foo"}, Value: util.Pointer(32.3)}, }, util.Pointer(1.0)), - outString: `[ var='0' metric='Test' labels={host=foo} value=32.3 ]`, + outString: `[ var='0' metric='Test' labels={host=foo} type='classic_conditions' value=32.3 ]`, }, { desc: "2 EvalMatches", @@ -29,7 +29,7 @@ func TestExtractEvalString(t *testing.T) { {Metric: "Test", Labels: data.Labels{"host": "foo"}, Value: util.Pointer(32.3)}, {Metric: "Test", Labels: data.Labels{"host": "baz"}, Value: util.Pointer(10.0)}, }, util.Pointer(1.0), withRefID("A")), - outString: `[ var='A0' metric='Test' labels={host=foo} value=32.3 ], [ var='A1' metric='Test' labels={host=baz} value=10 ]`, + outString: `[ var='A0' metric='Test' labels={host=foo} type='classic_conditions' value=32.3 ], [ var='A1' metric='Test' labels={host=baz} type='classic_conditions' value=10 ]`, }, { desc: "3 EvalMatches", @@ -38,15 +38,15 @@ func TestExtractEvalString(t *testing.T) { {Metric: "Test", Labels: data.Labels{"host": "baz"}, Value: util.Pointer(10.0)}, {Metric: "TestA", Labels: data.Labels{"host": "zip"}, Value: util.Pointer(11.0)}, }, util.Pointer(1.0), withRefID("A")), - outString: `[ var='A0' metric='Test' labels={host=foo} value=32.3 ], [ var='A1' metric='Test' labels={host=baz} value=10 ], [ var='A2' metric='TestA' labels={host=zip} value=11 ]`, + outString: `[ var='A0' metric='Test' labels={host=foo} type='classic_conditions' value=32.3 ], [ var='A1' metric='Test' labels={host=baz} type='classic_conditions' value=10 ], [ var='A2' metric='TestA' labels={host=zip} type='classic_conditions' value=11 ]`, }, { desc: "Captures are sorted in ascending order of var", inFrame: newMetaFrame([]NumberValueCapture{ - {Var: "B", Labels: data.Labels{"host": "foo"}, Value: util.Pointer(1.0)}, - {Var: "A", Labels: data.Labels{"host": "foo"}, Value: util.Pointer(10.0)}, + {Var: "B", Labels: data.Labels{"host": "foo"}, Value: util.Pointer(1.0), Type: "reduce"}, + {Var: "A", Labels: data.Labels{"host": "foo"}, Value: util.Pointer(10.0), Type: "threshold"}, }, util.Pointer(1.0)), - outString: `[ var='A' labels={host=foo} value=10 ], [ var='B' labels={host=foo} value=1 ]`, + outString: `[ var='A' labels={host=foo} type='threshold' value=10 ], [ var='B' labels={host=foo} type='reduce' value=1 ]`, }, } for _, tc := range cases { @@ -71,7 +71,7 @@ func TestExtractValues(t *testing.T) { {Metric: "A", Labels: data.Labels{"host": "foo"}, Value: util.Pointer(1.0)}, }, util.Pointer(1.0), withRefID("A")), values: map[string]NumberValueCapture{ - "A0": {Var: "A", Labels: data.Labels{"host": "foo"}, Value: util.Pointer(1.0)}, + "A0": {Var: "A", Labels: data.Labels{"host": "foo"}, Value: util.Pointer(1.0), Type: "classic_conditions"}, }, }, { desc: "Classic condition frame with multiple matches", @@ -80,8 +80,8 @@ func TestExtractValues(t *testing.T) { {Metric: "A", Labels: data.Labels{"host": "foo"}, Value: util.Pointer(3.0)}, }, util.Pointer(1.0), withRefID("A")), values: map[string]NumberValueCapture{ - "A0": {Var: "A", Labels: data.Labels{"host": "foo"}, Value: util.Pointer(1.0)}, - "A1": {Var: "A", Labels: data.Labels{"host": "foo"}, Value: util.Pointer(3.0)}, + "A0": {Var: "A", Labels: data.Labels{"host": "foo"}, Value: util.Pointer(1.0), Type: "classic_conditions"}, + "A1": {Var: "A", Labels: data.Labels{"host": "foo"}, Value: util.Pointer(3.0), Type: "classic_conditions"}, }, }, { desc: "Nil value", diff --git a/pkg/services/ngalert/models/alert_query.go b/pkg/services/ngalert/models/alert_query.go index f86925eaa9b..3f03ae2e68f 100644 --- a/pkg/services/ngalert/models/alert_query.go +++ b/pkg/services/ngalert/models/alert_query.go @@ -342,3 +342,31 @@ func (aq *AlertQuery) InitDefaults() error { aq.Model = model return nil } + +// GetExpressionType returns the type of expression for this AlertQuery. +// It returns "query" for regular datasource queries and the actual type for expressions. +func (aq *AlertQuery) GetExpressionType() (string, error) { + if aq.modelProps == nil { + err := aq.setModelProps() + if err != nil { + return "", err + } + } + + // Check if this is an expression query + isExpr, err := aq.IsExpression() + if err != nil { + return "", err + } + + if !isExpr { + return "query", nil // Regular data source query + } + + // Extract type from model + if exprType, ok := aq.modelProps["type"].(string); ok { + return exprType, nil + } + + return "unknown", nil +} diff --git a/pkg/services/ngalert/notifier/templates.go b/pkg/services/ngalert/notifier/templates.go index 5611bbe2b4c..761b982467c 100644 --- a/pkg/services/ngalert/notifier/templates.go +++ b/pkg/services/ngalert/notifier/templates.go @@ -20,7 +20,7 @@ var ( } DefaultAnnotations = map[string]string{ alertingModels.ValuesAnnotation: `{"B":22,"C":1}`, - alertingModels.ValueStringAnnotation: `[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]`, + alertingModels.ValueStringAnnotation: `[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} type='reduce' value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} type='threshold' value=1 ]`, alertingModels.OrgIDAnnotation: `1`, alertingModels.DashboardUIDAnnotation: `dashboard_uid`, alertingModels.PanelIDAnnotation: `1`, diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index 52c41586f31..b35bf6959c5 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -137,7 +137,7 @@ func TestIntegrationTestReceivers(t *testing.T) { "__dashboardUid__": "dashboard_uid", "__orgId__": "1", "__panelId__": "1", - "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} type='reduce' value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} type='threshold' value=1 ]", "__values__": "{\"B\":22,\"C\":1}" }, "labels": { @@ -225,7 +225,7 @@ func TestIntegrationTestReceivers(t *testing.T) { "__dashboardUid__": "dashboard_uid", "__orgId__": "1", "__panelId__": "1", - "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} type='reduce' value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} type='threshold' value=1 ]", "__values__": "{\"B\":22,\"C\":1}" }, "labels": { @@ -307,7 +307,7 @@ func TestIntegrationTestReceivers(t *testing.T) { "__dashboardUid__": "dashboard_uid", "__orgId__": "1", "__panelId__": "1", - "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} type='reduce' value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} type='threshold' value=1 ]", "__values__": "{\"B\":22,\"C\":1}" }, "labels": { @@ -400,7 +400,7 @@ func TestIntegrationTestReceivers(t *testing.T) { "__dashboardUid__": "dashboard_uid", "__orgId__": "1", "__panelId__": "1", - "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} type='reduce' value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} type='threshold' value=1 ]", "__values__": "{\"B\":22,\"C\":1}" }, "labels": { @@ -506,7 +506,7 @@ func TestIntegrationTestReceivers(t *testing.T) { "__dashboardUid__": "dashboard_uid", "__orgId__": "1", "__panelId__": "1", - "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} type='reduce' value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} type='threshold' value=1 ]", "__values__": "{\"B\":22,\"C\":1}" }, "labels": { @@ -805,7 +805,7 @@ func TestIntegrationTestReceiversAlertCustomization(t *testing.T) { "__dashboardUid__": "dashboard_uid", "__orgId__": "1", "__panelId__": "1", - "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} type='reduce' value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} type='threshold' value=1 ]", "__values__": "{\"B\":22,\"C\":1}" }, "labels": { @@ -2440,7 +2440,7 @@ var expEmailNotifications = []*notifications.SendEmailCommandSync{ PanelURL: "", OrgID: util.Pointer(int64(1)), Values: map[string]float64{"A": 1}, - ValueString: "[ var='A' labels={} value=1 ]", + ValueString: "[ var='A' labels={} type='math' value=1 ]", }, }, "GroupLabels": template.KV{"alertname": "EmailAlert"}, @@ -2594,10 +2594,10 @@ var expNonEmailNotifications = map[string][]string{ "grafana_folder": "default" }, "annotations": {}, - "startsAt": "%s", + "startsAt": "%s", "values": {"A": 1}, - "valueString": "[ var='A' labels={} value=1 ]", - "endsAt": "0001-01-01T00:00:00Z", + "valueString": "[ var='A' labels={} type='math' value=1 ]", + "endsAt": "0001-01-01T00:00:00Z", "generatorURL": "http://localhost:3000/alerting/grafana/UID_WebhookAlert/view?orgId=1", "fingerprint": "15c59b0a380bd9f1", "silenceURL": "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=__alert_rule_uid__%%3DUID_WebhookAlert&orgId=1", @@ -2768,10 +2768,10 @@ var expNonEmailNotifications = map[string][]string{ "alertname": "AlertmanagerAlert", "grafana_folder": "default" }, - "annotations": { - "__orgId__":"1", + "annotations": { + "__orgId__":"1", "__values__": "{\"A\":1}", - "__value_string__": "[ var='A' labels={} value=1 ]" + "__value_string__": "[ var='A' labels={} type='math' value=1 ]" }, "startsAt": "%s", "endsAt": "0001-01-01T00:00:00Z", diff --git a/pkg/tests/api/alerting/api_testing_test.go b/pkg/tests/api/alerting/api_testing_test.go index c35db49e569..fb14701e304 100644 --- a/pkg/tests/api/alerting/api_testing_test.go +++ b/pkg/tests/api/alerting/api_testing_test.go @@ -150,8 +150,8 @@ func TestGrafanaRuleConfig(t *testing.T) { for i, alert := range result { require.NotEmpty(t, alert.Annotations["values.B"]) require.NotEmpty(t, alert.Annotations["values.C"]) - valueB := fmt.Sprintf("[ var='B' labels={state=%s} value=%s ]", dynamicLabels[i], alert.Annotations["values.B"]) - valueC := fmt.Sprintf("[ var='C' labels={state=%s} value=%s ]", dynamicLabels[i], alert.Annotations["values.C"]) + valueB := fmt.Sprintf("[ var='B' labels={state=%s} type='reduce' value=%s ]", dynamicLabels[i], alert.Annotations["values.B"]) + valueC := fmt.Sprintf("[ var='C' labels={state=%s} type='threshold' value=%s ]", dynamicLabels[i], alert.Annotations["values.C"]) require.Contains(t, alert.Annotations["value"], valueB) require.Contains(t, alert.Annotations["value"], valueC) } @@ -172,8 +172,8 @@ func TestGrafanaRuleConfig(t *testing.T) { for i, alert := range result { require.NotEmpty(t, alert.Labels["values.B"]) require.NotEmpty(t, alert.Labels["values.C"]) - valueB := fmt.Sprintf("[ var='B' labels={state=%s} value=%s ]", dynamicLabels[i], alert.Labels["values.B"]) - valueC := fmt.Sprintf("[ var='C' labels={state=%s} value=%s ]", dynamicLabels[i], alert.Labels["values.C"]) + valueB := fmt.Sprintf("[ var='B' labels={state=%s} type='reduce' value=%s ]", dynamicLabels[i], alert.Labels["values.B"]) + valueC := fmt.Sprintf("[ var='C' labels={state=%s} type='threshold' value=%s ]", dynamicLabels[i], alert.Labels["values.C"]) require.Contains(t, alert.Labels["value"], valueB) require.Contains(t, alert.Labels["value"], valueC) } diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json index 1b716ad9033..567636ff7fc 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -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": "" diff --git a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx index d881d07f863..1c89c3c6dd4 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx +++ b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx @@ -6,10 +6,8 @@ import { Button, Drawer, Stack, Text } from '@grafana/ui'; import { appEvents } from 'app/core/app_events'; import { ManagerKind } from 'app/features/apiserver/types'; import { BulkDeleteProvisionedResource } from 'app/features/provisioning/components/BulkActions/BulkDeleteProvisionedResource'; -import { BulkExportProvisionedResource } from 'app/features/provisioning/components/BulkActions/BulkExportProvisionedResource'; import { BulkMoveProvisionedResource } from 'app/features/provisioning/components/BulkActions/BulkMoveProvisionedResource'; import { useSelectionProvisioningStatus } from 'app/features/provisioning/hooks/useSelectionProvisioningStatus'; -import { useSelectionUnmanagedStatus } from 'app/features/provisioning/hooks/useSelectionUnmanagedStatus'; import { useSearchStateManager } from 'app/features/search/state/SearchStateManager'; import { ShowModalReactEvent } from 'app/types/events'; import { FolderDTO } from 'app/types/folders'; @@ -20,7 +18,7 @@ import { useMoveMultipleFoldersMutationFacade, } from '../../../../api/clients/folder/v1beta1/hooks'; import { useDeleteDashboardsMutation, useMoveDashboardsMutation } from '../../api/browseDashboardsAPI'; -import { useActionSelectionState, useCheckboxSelectionState } from '../../state/hooks'; +import { useActionSelectionState } from '../../state/hooks'; import { setAllSelection } from '../../state/slice'; import { DashboardTreeSelection } from '../../types'; @@ -35,11 +33,9 @@ export interface Props { export function BrowseActions({ folderDTO }: Props) { const [showBulkDeleteProvisionedResource, setShowBulkDeleteProvisionedResource] = useState(false); const [showBulkMoveProvisionedResource, setShowBulkMoveProvisionedResource] = useState(false); - const [showBulkExportProvisionedResource, setShowBulkExportProvisionedResource] = useState(false); const dispatch = useDispatch(); - const selectedItemsForActions = useActionSelectionState(); // For move/delete - filters out children - const selectedItems = useCheckboxSelectionState(); // For export - includes all selected items + const selectedItems = useActionSelectionState(); const [deleteDashboards] = useDeleteDashboardsMutation(); const deleteFolders = useDeleteMultipleFoldersMutationFacade(); const [moveFolders] = useMoveMultipleFoldersMutationFacade(); @@ -48,10 +44,9 @@ export function BrowseActions({ folderDTO }: Props) { const provisioningEnabled = config.featureToggles.provisioning; const { hasProvisioned, hasNonProvisioned } = useSelectionProvisioningStatus( - selectedItemsForActions, + selectedItems, folderDTO?.managedBy === ManagerKind.Repo ); - const { hasUnmanaged, isLoading: isLoadingUnmanaged } = useSelectionUnmanagedStatus(selectedItems); const isSearching = stateManager.hasSearchFilters(); @@ -65,29 +60,21 @@ export function BrowseActions({ folderDTO }: Props) { }; const onDelete = async () => { - const selectedDashboards = Object.keys(selectedItemsForActions.dashboard).filter( - (uid) => selectedItemsForActions.dashboard[uid] - ); - const selectedFolders = Object.keys(selectedItemsForActions.folder).filter( - (uid) => selectedItemsForActions.folder[uid] - ); + const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]); + const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]); await deleteDashboards({ dashboardUIDs: selectedDashboards }); await deleteFolders({ folderUIDs: selectedFolders }); - trackAction('delete', selectedItemsForActions); + trackAction('delete', selectedItems); onActionComplete(); }; const onMove = async (destinationUID: string) => { - const selectedDashboards = Object.keys(selectedItemsForActions.dashboard).filter( - (uid) => selectedItemsForActions.dashboard[uid] - ); - const selectedFolders = Object.keys(selectedItemsForActions.folder).filter( - (uid) => selectedItemsForActions.folder[uid] - ); + const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]); + const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]); await moveFolders({ folderUIDs: selectedFolders, destinationUID }); await moveDashboards({ dashboardUIDs: selectedDashboards, destinationUID }); - trackAction('move', selectedItemsForActions); + trackAction('move', selectedItems); onActionComplete(); }; @@ -153,26 +140,10 @@ export function BrowseActions({ folderDTO }: Props) { ); - // Check if any dashboards are selected (export only supports dashboards, not folders) - // Use raw selectedItems (not selectedItemsForActions) to include all selected dashboards - const hasSelectedDashboards = - Object.keys(selectedItems.dashboard || {}).filter((uid) => selectedItems.dashboard[uid]).length > 0; - - const pushButton = ( - - ); - return ( <> {moveButton} - {provisioningEnabled && pushButton} - {provisioningEnabled && isUnmanaged && ( - - )} @@ -112,30 +94,6 @@ export const ShareExport = memo(({ dashboard, panel, onDismiss }: Props) => { Save to file - {showExportToRepositoryDrawer && ( - - {t('share-modal.export.export-to-repository-title', 'Export Dashboard to Repository')} - - } - subtitle={dashboard.title} - onClose={() => setShowExportToRepositoryDrawer(false)} - size="md" - > - { - setShowExportToRepositoryDrawer(false); - onDismiss?.(); - }} - /> - - )} ); }); diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 0f54e455b41..eb0c6eba3a0 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -5226,7 +5226,6 @@ }, "only-overrides-button-tooltip": "Zobrazit pouze přepsání", "placeholder-search-options": "Možnosti hledání", - "visualization-button-label": "Vizualizace", "visualization-button-tooltip": "Možnosti hledání" }, "panel-editor-table-view": { @@ -6269,6 +6268,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "Hledat…", "radio-options": { "label": { @@ -6582,7 +6584,7 @@ }, "visualization-button": { "aria-label-change-visualization": "Změnit vizualizaci", - "tooltip-click-to-change-visualization": "Klikněte pro změnu vizualizace" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "Otevřít podokno dotazu", @@ -7378,7 +7380,8 @@ "title-failed-volume-query": "Objem protokolu pro tento dotaz se nepodařilo načíst", "title-no-logs-volume-available": "Není k dispozici žádný objem protokolů", "title-showing-partial-data": "Zobrazují se částečná data", - "title-unable-to-show-log-volume": "Nelze zobrazit objem protokolu" + "title-unable-to-show-log-volume": "Nelze zobrazit objem protokolu", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "Nejsou dostupné žádné informace o objemu pro aktuální dotazy a časový rozsah." @@ -11182,6 +11185,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": { diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 075df04b5d0..4f5a351736c 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -5184,7 +5184,6 @@ }, "only-overrides-button-tooltip": "Nur Überschreibungen anzeigen", "placeholder-search-options": "Suchoptionen", - "visualization-button-label": "Visualisierung", "visualization-button-tooltip": "Suchoptionen" }, "panel-editor-table-view": { @@ -6223,6 +6222,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "Suche nach ...", "radio-options": { "label": { @@ -6534,7 +6536,7 @@ }, "visualization-button": { "aria-label-change-visualization": "Visualisierung ändern", - "tooltip-click-to-change-visualization": "Klicken, um Visualisierung zu ändern" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "Abfragebereich öffnen", @@ -7330,7 +7332,8 @@ "title-failed-volume-query": "Das Log-Volumen für diese Abfrage konnte nicht geladen werden", "title-no-logs-volume-available": "Kein Logs-Volumen verfügbar", "title-showing-partial-data": "Teildaten werden angezeigt", - "title-unable-to-show-log-volume": "Log-Volumen kann nicht angezeigt werden" + "title-unable-to-show-log-volume": "Log-Volumen kann nicht angezeigt werden", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "Für die aktuellen Abfragen und den aktuellen Zeitbereich sind keine Volumeninformationen verfügbar." @@ -11088,6 +11091,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index d630a41b1dc..1253a3ecf56 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -3563,9 +3563,6 @@ "delete-modal-title": "Delete", "delete-provisioned-folder": "Delete provisioned folder", "deleting": "Deleting...", - "export-folder": "Export Folder to Repository", - "export-provisioned-resources": "Export Resources", - "export-to-repository-button": "Export to Repository", "manage-permissions-button": "Manage permissions", "move-button": "Move", "move-modal-alert": "Moving this item may change its permissions.", @@ -3596,29 +3593,6 @@ "delete-warning": "This will delete selected folders and their descendants. In total, this will affect:", "error-deleting-resources": "Error deleting resources" }, - "bulk-export-resources-form": { - "button-cancel": "Cancel", - "button-export": "Export", - "button-exporting": "Exporting...", - "dashboards-count_one": "{{count}} dashboard", - "dashboards-count_other": "{{count}} dashboard", - "error-exporting-resources": "Error exporting resources", - "error-no-dashboards": "No dashboards selected. Only dashboards can be exported.", - "error-no-repository": "Please select a repository", - "export-total": "In total, this will export:", - "folders-count_one": "{{count}} folder", - "folders-count_other": "{{count}} folder", - "folders-info": "Folders in selection", - "folders-info-description": "Folders will be left behind. New folders will be created in the repository based on the resource folder structure.", - "no-items": "No items selected", - "path": "Path", - "path-description": "Path relative to the repository root (optional). Resources will be exported under this path.", - "path-description-with-repo": "Add a sub-path below to organize exported resources.", - "path-placeholder": "e.g., dashboards/", - "path-placeholder-with-repo": "e.g., dashboards/team-a/", - "repository": "Repository", - "repository-placeholder": "Select a repository" - }, "bulk-move-resources-form": { "button-cancel": "Cancel", "button-move": "Move", @@ -3684,8 +3658,6 @@ "folder-actions-button": { "delete": "Delete this folder", "delete-folder-error": "Error deleting folder. Please try again later.", - "export": "Export to Repository", - "export-folder-error": "Error collecting dashboards. Please try again later.", "folder-actions": "Folder actions", "manage-permissions": "Manage permissions", "move": "Move this folder" @@ -6587,7 +6559,7 @@ "delete-button": "Delete", "title": "Delete" }, - "delete-modal-restore-dashboards-text": "This action will mark the dashboard for deletion in 30 days. Your organization administrator can restore it anytime before the 30 days expire.", + "delete-modal-restore-dashboards-text": "This action will delete the dashboard. Deleted dashboards will be kept in the history for up to 12 months and can be restored by your organization administrator during that time. The history is limited to 1000 dashboards—older ones will be removed sooner if the limit is reached.", "delete-modal-text": "Do you want to delete this dashboard?", "general": { "auto-refresh-description": "Define the auto refresh intervals that should be available in the auto refresh list. Use the format '5s' for seconds, '1m' for minutes, '1h' for hours, and '1d' for days (e.g.: '5s,10s,30s,1m,5m,15m,30m,1h,2h,1d').", @@ -11765,7 +11737,6 @@ "folder-repository-list": { "all-resources-managed_one": "All {{count}} resource is managed", "all-resources-managed_other": "All {{count}} resources are managed", - "export-remaining-resources-button": "Export remaining resources", "no-results-matching-your-query": "No results matching your query", "partial-managed": "{{managedCount}}/{{resourceCount}} resources managed by Git sync.", "placeholder-search": "Search", @@ -12742,7 +12713,6 @@ "menu": { "export-image-title": "Export as image", "export-json-title": "Export as JSON", - "export-to-repository-title": "Export to Repository", "share-externally-title": "Share externally", "share-internally-title": "Share internally", "share-snapshot-title": "Share snapshot" @@ -12770,8 +12740,6 @@ "export": { "back-button": "Back to export config", "cancel-button": "Cancel", - "export-to-repository-button": "Export to Repository", - "export-to-repository-title": "Export Dashboard to Repository", "info-text": "Export this dashboard.", "loading": "Loading...", "save-button": "Save to file", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index d5ef82955ed..bc1ccea444c 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -5184,7 +5184,6 @@ }, "only-overrides-button-tooltip": "Mostrar solo anulaciones", "placeholder-search-options": "Opciones de búsqueda", - "visualization-button-label": "Visualización", "visualization-button-tooltip": "Opciones de búsqueda" }, "panel-editor-table-view": { @@ -6223,6 +6222,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "Buscar...", "radio-options": { "label": { @@ -6534,7 +6536,7 @@ }, "visualization-button": { "aria-label-change-visualization": "Cambiar visualización", - "tooltip-click-to-change-visualization": "Haz clic para cambiar la visualización" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "Abrir panel de consulta", @@ -7330,7 +7332,8 @@ "title-failed-volume-query": "Error al cargar el volumen de logs para esta consulta", "title-no-logs-volume-available": "No hay volumen de logs disponible", "title-showing-partial-data": "Mostrando datos parciales", - "title-unable-to-show-log-volume": "No se puede mostrar el volumen de logs" + "title-unable-to-show-log-volume": "No se puede mostrar el volumen de logs", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "No hay información de volumen disponible para las consultas y el intervalo de tiempo actuales." @@ -11088,6 +11091,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": { diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 7849934975c..50f4e61edd0 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -5184,7 +5184,6 @@ }, "only-overrides-button-tooltip": "Afficher uniquement les remplacements", "placeholder-search-options": "Options de recherche", - "visualization-button-label": "Visualisation", "visualization-button-tooltip": "Options de recherche" }, "panel-editor-table-view": { @@ -6223,6 +6222,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "Rechercher…", "radio-options": { "label": { @@ -6534,7 +6536,7 @@ }, "visualization-button": { "aria-label-change-visualization": "Modifier la visualisation", - "tooltip-click-to-change-visualization": "Cliquer pour modifier la visualisation" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "Ouvrir le volet de requête", @@ -7330,7 +7332,8 @@ "title-failed-volume-query": "Échec du chargement du volume de journal pour cette requête", "title-no-logs-volume-available": "Aucun volume de journaux disponible", "title-showing-partial-data": "Affichage de données partielles", - "title-unable-to-show-log-volume": "Impossible d’afficher le volume de journaux" + "title-unable-to-show-log-volume": "Impossible d’afficher le volume de journaux", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "Aucune information de volume disponible pour les requêtes actuelles et la plage temporelle." @@ -11088,6 +11091,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": { diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 800a0de556e..47ccb2d223e 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -5184,7 +5184,6 @@ }, "only-overrides-button-tooltip": "Csak a felülbírálások megjelenítése", "placeholder-search-options": "Keresési beállítások", - "visualization-button-label": "Vizualizáció", "visualization-button-tooltip": "Keresési beállítások" }, "panel-editor-table-view": { @@ -6223,6 +6222,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "Keresés a következőre…", "radio-options": { "label": { @@ -6534,7 +6536,7 @@ }, "visualization-button": { "aria-label-change-visualization": "Vizualizáció módosítása", - "tooltip-click-to-change-visualization": "Kattintson a vizualizáció módosításához" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "Lekérdezési ablaktábla megnyitása", @@ -7330,7 +7332,8 @@ "title-failed-volume-query": "A lekérdezés naplómennyiségének betöltése nem sikerült", "title-no-logs-volume-available": "Nincs rendelkezésre álló naplómennyiség", "title-showing-partial-data": "Részadatok megjelenítése", - "title-unable-to-show-log-volume": "Nem lehet megjeleníteni a naplómennyiséget" + "title-unable-to-show-log-volume": "Nem lehet megjeleníteni a naplómennyiséget", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "Nem áll rendelkezésre mennyiséginformáció az aktuális lekérdezésekhez és időtartományhoz." @@ -11088,6 +11091,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": { diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index edf158ffbd1..516ecb7f2a0 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -5163,7 +5163,6 @@ }, "only-overrides-button-tooltip": "Tampilkan penimpaan saja", "placeholder-search-options": "Opsi pencarian", - "visualization-button-label": "Visualisasi", "visualization-button-tooltip": "Opsi pencarian" }, "panel-editor-table-view": { @@ -6200,6 +6199,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "Cari...", "radio-options": { "label": { @@ -6510,7 +6512,7 @@ }, "visualization-button": { "aria-label-change-visualization": "Ubah visualisasi", - "tooltip-click-to-change-visualization": "Klik untuk mengubah visualisasi" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "Buka panel kueri", @@ -7306,7 +7308,8 @@ "title-failed-volume-query": "Gagal memuat volume log untuk kueri ini", "title-no-logs-volume-available": "Tidak ada volume log yang tersedia", "title-showing-partial-data": "Menampilkan data parsial", - "title-unable-to-show-log-volume": "Tidak dapat menampilkan volume log" + "title-unable-to-show-log-volume": "Tidak dapat menampilkan volume log", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "Tidak ada informasi volume yang tersedia untuk kueri dan rentang waktu saat ini." @@ -11041,6 +11044,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": { diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index c0c2e9cfe0d..b5fc787b5de 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -5184,7 +5184,6 @@ }, "only-overrides-button-tooltip": "Mostra solo sovrascritture", "placeholder-search-options": "Opzioni di ricerca", - "visualization-button-label": "Visualizzazione", "visualization-button-tooltip": "Opzioni di ricerca" }, "panel-editor-table-view": { @@ -6223,6 +6222,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "Cerca...", "radio-options": { "label": { @@ -6534,7 +6536,7 @@ }, "visualization-button": { "aria-label-change-visualization": "Cambia visualizzazione", - "tooltip-click-to-change-visualization": "Fai clic per cambiare la visualizzazione" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "Apri il riquadro della query", @@ -7330,7 +7332,8 @@ "title-failed-volume-query": "Impossibile caricare il volume dei registri per questa query", "title-no-logs-volume-available": "Nessun volume di registri disponibile", "title-showing-partial-data": "Visualizzazione di dati parziali", - "title-unable-to-show-log-volume": "Impossibile mostrare il volume del registro" + "title-unable-to-show-log-volume": "Impossibile mostrare il volume del registro", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "Nessuna informazione sul volume disponibile per le query e l'intervallo di tempo correnti." @@ -11088,6 +11091,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": { diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 6736b3e5fb0..a99080cb0f4 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -5163,7 +5163,6 @@ }, "only-overrides-button-tooltip": "オーバーライドのみ表示", "placeholder-search-options": "検索オプション", - "visualization-button-label": "‌可視化", "visualization-button-tooltip": "検索オプション" }, "panel-editor-table-view": { @@ -6200,6 +6199,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "次を検索...", "radio-options": { "label": { @@ -6510,7 +6512,7 @@ }, "visualization-button": { "aria-label-change-visualization": "可視化を変更", - "tooltip-click-to-change-visualization": "クリックして可視化を変更" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "クエリペインを開く", @@ -7306,7 +7308,8 @@ "title-failed-volume-query": "このクエリのログ量の読み込みに失敗しました", "title-no-logs-volume-available": "利用可能なログ量がありません", "title-showing-partial-data": "部分的なデータの表示", - "title-unable-to-show-log-volume": "ログ量を表示できません" + "title-unable-to-show-log-volume": "ログ量を表示できません", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "現在のクエリと時間範囲で利用可能なログ量情報はありません。" @@ -11041,6 +11044,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": { diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index d17b3191e98..82f4cf39e1b 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -5163,7 +5163,6 @@ }, "only-overrides-button-tooltip": "재정의만 표시", "placeholder-search-options": "검색 옵션", - "visualization-button-label": "시각화", "visualization-button-tooltip": "검색 옵션" }, "panel-editor-table-view": { @@ -6200,6 +6199,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "검색…", "radio-options": { "label": { @@ -6510,7 +6512,7 @@ }, "visualization-button": { "aria-label-change-visualization": "시각화 변경", - "tooltip-click-to-change-visualization": "시각화를 변경하려면 클릭하세요" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "쿼리 창 열기", @@ -7306,7 +7308,8 @@ "title-failed-volume-query": "이 쿼리에 대한 로그 볼륨 로딩 실패", "title-no-logs-volume-available": "사용 가능한 로그 볼륨 없음", "title-showing-partial-data": "부분 데이터 표시 중", - "title-unable-to-show-log-volume": "로그 볼륨을 표시할 수 없습니다" + "title-unable-to-show-log-volume": "로그 볼륨을 표시할 수 없습니다", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "현재 쿼리 및 시간 범위에 대해 사용할 수 있는 볼륨 정보가 없습니다." @@ -11041,6 +11044,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": { diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 5339e9cc1ba..332e058e99e 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -5184,7 +5184,6 @@ }, "only-overrides-button-tooltip": "Alleen overschrijvingen weergeven", "placeholder-search-options": "Zoekopties", - "visualization-button-label": "Visualisatie", "visualization-button-tooltip": "Zoekopties" }, "panel-editor-table-view": { @@ -6223,6 +6222,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "Zoeken naar...", "radio-options": { "label": { @@ -6534,7 +6536,7 @@ }, "visualization-button": { "aria-label-change-visualization": "Visualisatie wijzigen", - "tooltip-click-to-change-visualization": "Klik om de visualisatie te wijzigen" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "Querydeelvenster openen", @@ -7330,7 +7332,8 @@ "title-failed-volume-query": "Kan het logvolume voor deze query niet laden", "title-no-logs-volume-available": "Geen logvolume beschikbaar", "title-showing-partial-data": "Gedeeltelijke gegevens weergeven", - "title-unable-to-show-log-volume": "Kan logvolume niet weergeven" + "title-unable-to-show-log-volume": "Kan logvolume niet weergeven", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "Geen volume-informatie beschikbaar voor de huidige query's en het huidige tijdbereik." @@ -11088,6 +11091,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": { diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 9a90219b85f..e3c3dc215c4 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -5226,7 +5226,6 @@ }, "only-overrides-button-tooltip": "Pokaż tylko zastąpienia", "placeholder-search-options": "Opcje wyszukiwania", - "visualization-button-label": "Wizualizacja", "visualization-button-tooltip": "Opcje wyszukiwania" }, "panel-editor-table-view": { @@ -6269,6 +6268,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "Szukaj…", "radio-options": { "label": { @@ -6582,7 +6584,7 @@ }, "visualization-button": { "aria-label-change-visualization": "Zmień wizualizację", - "tooltip-click-to-change-visualization": "Kliknij, aby zmienić wizualizację" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "Otwórz okienko zapytania", @@ -7378,7 +7380,8 @@ "title-failed-volume-query": "Nie udało się załadować woluminu logów dla tego zapytania", "title-no-logs-volume-available": "Brak dostępnego woluminu logów", "title-showing-partial-data": "Wyświetlanie częściowych danych", - "title-unable-to-show-log-volume": "Nie można wyświetlić woluminu logów" + "title-unable-to-show-log-volume": "Nie można wyświetlić woluminu logów", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "Brak dostępnych informacji o wolumenie dla bieżących zapytań i zakresu czasu." @@ -11182,6 +11185,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": { diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index b3d6aa4c02f..c74388e4232 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -5184,7 +5184,6 @@ }, "only-overrides-button-tooltip": "Exibir apenas substituições", "placeholder-search-options": "Opções de pesquisa", - "visualization-button-label": "Visualização", "visualization-button-tooltip": "Opções de pesquisa" }, "panel-editor-table-view": { @@ -6223,6 +6222,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "Pesquisar por…", "radio-options": { "label": { @@ -6534,7 +6536,7 @@ }, "visualization-button": { "aria-label-change-visualization": "Alterar visualização", - "tooltip-click-to-change-visualization": "Clique para alterar a visualização" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "Abrir painel de consulta", @@ -7330,7 +7332,8 @@ "title-failed-volume-query": "Falha ao carregar o volume de log para esta consulta", "title-no-logs-volume-available": "Nenhum volume de logs disponível", "title-showing-partial-data": "Exibindo dados parciais", - "title-unable-to-show-log-volume": "Não é possível exibir o volume de log" + "title-unable-to-show-log-volume": "Não é possível exibir o volume de log", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "Nenhuma informação de volume disponível para as consultas atuais e intervalo de tempo." @@ -11088,6 +11091,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": { diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 744661bf91c..a82cfe5b432 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -5184,7 +5184,6 @@ }, "only-overrides-button-tooltip": "Mostrar apenas as substituições", "placeholder-search-options": "Opções de pesquisa", - "visualization-button-label": "Visualização", "visualization-button-tooltip": "Opções de pesquisa" }, "panel-editor-table-view": { @@ -6223,6 +6222,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "Pesquisar por...", "radio-options": { "label": { @@ -6534,7 +6536,7 @@ }, "visualization-button": { "aria-label-change-visualization": "Alterar a visualização", - "tooltip-click-to-change-visualization": "Clique para alterar a visualização" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "Abrir painel de consulta", @@ -7330,7 +7332,8 @@ "title-failed-volume-query": "Falha ao carregar o volume de registos para esta consulta", "title-no-logs-volume-available": "Nenhum volume de registos disponível", "title-showing-partial-data": "A mostrar dados parciais", - "title-unable-to-show-log-volume": "Não foi possível mostrar o volume de registo" + "title-unable-to-show-log-volume": "Não foi possível mostrar o volume de registo", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "Não há informações de volume disponíveis para as consultas atuais e intervalo de tempo." @@ -11088,6 +11091,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": { diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index 0a2820e3e6c..d9de61dde42 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -5226,7 +5226,6 @@ }, "only-overrides-button-tooltip": "Показывать только переопределения", "placeholder-search-options": "Поиск параметров", - "visualization-button-label": "Визуализация", "visualization-button-tooltip": "Поиск параметров" }, "panel-editor-table-view": { @@ -6269,6 +6268,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "Поиск...", "radio-options": { "label": { @@ -6582,7 +6584,7 @@ }, "visualization-button": { "aria-label-change-visualization": "Изменить визуализацию", - "tooltip-click-to-change-visualization": "Нажмите, чтобы изменить визуализацию" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "Открыть панель запросов", @@ -7378,7 +7380,8 @@ "title-failed-volume-query": "Не удалось загрузить объем журналов для этого запроса", "title-no-logs-volume-available": "Нет доступного объема журналов", "title-showing-partial-data": "Отображение частичных данных", - "title-unable-to-show-log-volume": "Не удалось показать объем журналов" + "title-unable-to-show-log-volume": "Не удалось показать объем журналов", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "Информация об объеме недоступна для текущих запросов и временного диапазона." @@ -11182,6 +11185,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": { diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index b7b1cdf6cfa..37702588d0a 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -5184,7 +5184,6 @@ }, "only-overrides-button-tooltip": "Visa endast åsidosättningar", "placeholder-search-options": "Sökalternativ", - "visualization-button-label": "Visualisering", "visualization-button-tooltip": "Sökalternativ" }, "panel-editor-table-view": { @@ -6223,6 +6222,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "Sök efter …", "radio-options": { "label": { @@ -6534,7 +6536,7 @@ }, "visualization-button": { "aria-label-change-visualization": "Ändra visualisering", - "tooltip-click-to-change-visualization": "Klicka för att ändra visualisering" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "Öppna frågefönstret", @@ -7330,7 +7332,8 @@ "title-failed-volume-query": "Det gick inte att ladda loggvolym för denna fråga", "title-no-logs-volume-available": "Ingen loggvolym tillgänglig", "title-showing-partial-data": "Visar partiella data", - "title-unable-to-show-log-volume": "Det gick inte att visa loggvolym" + "title-unable-to-show-log-volume": "Det gick inte att visa loggvolym", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "Ingen volyminformation tillgänglig för de aktuella frågorna och tidsintervallet." @@ -11088,6 +11091,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": { diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 6c49826ac02..ea77d97bfd9 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -5184,7 +5184,6 @@ }, "only-overrides-button-tooltip": "Sadece geçersiz kılmaları göster", "placeholder-search-options": "Arama seçenekleri", - "visualization-button-label": "Görselleştirme", "visualization-button-tooltip": "Arama seçenekleri" }, "panel-editor-table-view": { @@ -6223,6 +6222,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "Ara...", "radio-options": { "label": { @@ -6534,7 +6536,7 @@ }, "visualization-button": { "aria-label-change-visualization": "Görselleştirmeyi değiştir", - "tooltip-click-to-change-visualization": "Görselleştirmeyi değiştirmek için tıklayın" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "Sorgu bölmesini aç", @@ -7330,7 +7332,8 @@ "title-failed-volume-query": "Bu sorgu için günlük kaydı birimi yüklenemedi", "title-no-logs-volume-available": "Günlük kaydı birimi yok", "title-showing-partial-data": "Kısmi veriler gösteriliyor", - "title-unable-to-show-log-volume": "Günlük kaydı birimi gösterilemiyor" + "title-unable-to-show-log-volume": "Günlük kaydı birimi gösterilemiyor", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "Mevcut sorgular ve zaman aralığı için birim bilgisi mevcut değil." @@ -11088,6 +11091,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": { diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 7168af3ce09..950acdeadbb 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -5163,7 +5163,6 @@ }, "only-overrides-button-tooltip": "仅显示覆盖", "placeholder-search-options": "搜索选项", - "visualization-button-label": "可视化", "visualization-button-tooltip": "搜索选项" }, "panel-editor-table-view": { @@ -6200,6 +6199,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "搜索...", "radio-options": { "label": { @@ -6510,7 +6512,7 @@ }, "visualization-button": { "aria-label-change-visualization": "更改可视化", - "tooltip-click-to-change-visualization": "点击可更改可视化" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "打开查询窗格", @@ -7306,7 +7308,8 @@ "title-failed-volume-query": "加载此查询的日志卷失败", "title-no-logs-volume-available": "没有可用的日志卷", "title-showing-partial-data": "显示部分数据", - "title-unable-to-show-log-volume": "无法显示日志卷" + "title-unable-to-show-log-volume": "无法显示日志卷", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "没有可用于当前查询和时间范围的卷信息。" @@ -11041,6 +11044,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": { diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index dddcce4ef55..f4cdba753fd 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -5163,7 +5163,6 @@ }, "only-overrides-button-tooltip": "僅顯示覆寫", "placeholder-search-options": "搜尋選項", - "visualization-button-label": "可視化", "visualization-button-tooltip": "搜尋選項" }, "panel-editor-table-view": { @@ -6200,6 +6199,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "" + }, "placeholder-search-for": "搜尋…", "radio-options": { "label": { @@ -6510,7 +6512,7 @@ }, "visualization-button": { "aria-label-change-visualization": "變更視覺效果", - "tooltip-click-to-change-visualization": "按一下以變更視覺效果" + "text": "" }, "viz-and-data-pane": { "aria-label-open-query-pane": "開啟查詢窗格", @@ -7306,7 +7308,8 @@ "title-failed-volume-query": "無法載入此查詢的紀錄容量", "title-no-logs-volume-available": "沒有可用的紀錄容量", "title-showing-partial-data": "顯示部分資料", - "title-unable-to-show-log-volume": "無法顯示紀錄容量" + "title-unable-to-show-log-volume": "無法顯示紀錄容量", + "visible-range-description": "" }, "logs-volumne-panel-list": { "body-no-logs-volume-available": "目前的查詢和時間範圍沒有可用的容量資訊。" @@ -11041,6 +11044,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "", + "error-loading-suggestions": { + "message": "", + "title": "" + }, + "unknown-viz-type": "", "use-this-suggestion": "" }, "viz-type-picker": {