chore: restore unrelated files from origin/main
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,22 @@ AnnotationPanelFilter: {
|
||||
ids: [...uint32]
|
||||
}
|
||||
|
||||
// Annotation event field source. Defines how to obtain the value for an annotation event field.
|
||||
// - "field": Find the value with a matching key (default)
|
||||
// - "text": Write a constant string into the value
|
||||
// - "skip": Do not include the field
|
||||
AnnotationEventFieldSource: "field" | "text" | "skip" | *"field"
|
||||
|
||||
// Annotation event field mapping. Defines how to map a data frame field to an annotation event field.
|
||||
AnnotationEventFieldMapping: {
|
||||
// Source type for the field value
|
||||
source?: AnnotationEventFieldSource | *"field"
|
||||
// Constant value to use when source is "text"
|
||||
value?: string
|
||||
// Regular expression to apply to the field value
|
||||
regex?: string
|
||||
}
|
||||
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
// "Crosshair" for shared crosshair.
|
||||
// "Tooltip" for shared crosshair AND shared tooltip.
|
||||
@@ -451,7 +467,10 @@ AnnotationQuerySpec: {
|
||||
name: string
|
||||
builtIn?: bool | *false
|
||||
filter?: AnnotationPanelFilter
|
||||
legacyOptions?: [string]: _ //Catch-all field for datasource-specific properties
|
||||
// Mappings define how to convert data frame fields to annotation event fields.
|
||||
mappings?: [string]: AnnotationEventFieldMapping
|
||||
// Catch-all field for datasource-specific properties
|
||||
legacyOptions?: [string]: _
|
||||
}
|
||||
|
||||
AnnotationQueryKind: {
|
||||
|
||||
@@ -75,10 +75,26 @@ LibraryPanelRef: {
|
||||
|
||||
AnnotationPanelFilter: {
|
||||
// Should the specified panels be included or excluded
|
||||
exclude?: bool | *false
|
||||
exclude?: bool | *false
|
||||
|
||||
// Panel IDs that should be included or excluded
|
||||
ids: [...uint32]
|
||||
}
|
||||
|
||||
// Annotation event field source. Defines how to obtain the value for an annotation event field.
|
||||
// - "field": Find the value with a matching key (default)
|
||||
// - "text": Write a constant string into the value
|
||||
// - "skip": Do not include the field
|
||||
AnnotationEventFieldSource: "field" | "text" | "skip" | *"field"
|
||||
|
||||
// Annotation event field mapping. Defines how to map a data frame field to an annotation event field.
|
||||
AnnotationEventFieldMapping: {
|
||||
// Source type for the field value
|
||||
source?: AnnotationEventFieldSource | *"field"
|
||||
// Constant value to use when source is "text"
|
||||
value?: string
|
||||
// Regular expression to apply to the field value
|
||||
regex?: string
|
||||
}
|
||||
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
@@ -450,7 +466,10 @@ AnnotationQuerySpec: {
|
||||
filter?: AnnotationPanelFilter
|
||||
// Placement can be used to display the annotation query somewhere else on the dashboard other than the default location.
|
||||
placement?: AnnotationQueryPlacement
|
||||
legacyOptions?: [string]: _ // Catch-all field for datasource-specific properties. Should not be available in as code tooling.
|
||||
// Mappings define how to convert data frame fields to annotation event fields.
|
||||
mappings?: [string]: AnnotationEventFieldMapping
|
||||
// Catch-all field for datasource-specific properties. Should not be available in as code tooling.
|
||||
legacyOptions?: [string]: _
|
||||
}
|
||||
|
||||
AnnotationQueryKind: {
|
||||
|
||||
@@ -85,6 +85,22 @@ AnnotationPanelFilter: {
|
||||
ids: [...uint32]
|
||||
}
|
||||
|
||||
// Annotation event field source. Defines how to obtain the value for an annotation event field.
|
||||
// - "field": Find the value with a matching key (default)
|
||||
// - "text": Write a constant string into the value
|
||||
// - "skip": Do not include the field
|
||||
AnnotationEventFieldSource: "field" | "text" | "skip" | *"field"
|
||||
|
||||
// Annotation event field mapping. Defines how to map a data frame field to an annotation event field.
|
||||
AnnotationEventFieldMapping: {
|
||||
// Source type for the field value
|
||||
source?: AnnotationEventFieldSource | *"field"
|
||||
// Constant value to use when source is "text"
|
||||
value?: string
|
||||
// Regular expression to apply to the field value
|
||||
regex?: string
|
||||
}
|
||||
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
// "Crosshair" for shared crosshair.
|
||||
// "Tooltip" for shared crosshair AND shared tooltip.
|
||||
@@ -455,7 +471,10 @@ AnnotationQuerySpec: {
|
||||
name: string
|
||||
builtIn?: bool | *false
|
||||
filter?: AnnotationPanelFilter
|
||||
legacyOptions?: [string]: _ //Catch-all field for datasource-specific properties
|
||||
// Mappings define how to convert data frame fields to annotation event fields.
|
||||
mappings?: [string]: AnnotationEventFieldMapping
|
||||
// Catch-all field for datasource-specific properties
|
||||
legacyOptions?: [string]: _
|
||||
}
|
||||
|
||||
AnnotationQueryKind: {
|
||||
|
||||
@@ -31,6 +31,8 @@ type DashboardAnnotationQuerySpec struct {
|
||||
Name string `json:"name"`
|
||||
BuiltIn *bool `json:"builtIn,omitempty"`
|
||||
Filter *DashboardAnnotationPanelFilter `json:"filter,omitempty"`
|
||||
// Mappings define how to convert data frame fields to annotation event fields.
|
||||
Mappings map[string]DashboardAnnotationEventFieldMapping `json:"mappings,omitempty"`
|
||||
// Catch-all field for datasource-specific properties
|
||||
LegacyOptions map[string]interface{} `json:"legacyOptions,omitempty"`
|
||||
}
|
||||
@@ -85,6 +87,24 @@ func NewDashboardAnnotationPanelFilter() *DashboardAnnotationPanelFilter {
|
||||
}
|
||||
}
|
||||
|
||||
// Annotation event field mapping. Defines how to map a data frame field to an annotation event field.
|
||||
// +k8s:openapi-gen=true
|
||||
type DashboardAnnotationEventFieldMapping struct {
|
||||
// Source type for the field value
|
||||
Source *string `json:"source,omitempty"`
|
||||
// Constant value to use when source is "text"
|
||||
Value *string `json:"value,omitempty"`
|
||||
// Regular expression to apply to the field value
|
||||
Regex *string `json:"regex,omitempty"`
|
||||
}
|
||||
|
||||
// NewDashboardAnnotationEventFieldMapping creates a new DashboardAnnotationEventFieldMapping object.
|
||||
func NewDashboardAnnotationEventFieldMapping() *DashboardAnnotationEventFieldMapping {
|
||||
return &DashboardAnnotationEventFieldMapping{
|
||||
Source: (func(input string) *string { return &input })("field"),
|
||||
}
|
||||
}
|
||||
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
// "Crosshair" for shared crosshair.
|
||||
// "Tooltip" for shared crosshair AND shared tooltip.
|
||||
|
||||
@@ -23,6 +23,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdHocFilterWithLabels": schema_pkg_apis_dashboard_v2alpha1_DashboardAdHocFilterWithLabels(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableKind(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableSpec(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationEventFieldMapping": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationEventFieldMapping(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationPanelFilter": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationPanelFilter(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQueryKind(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQuerySpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQuerySpec(ref),
|
||||
@@ -644,6 +645,40 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableSpec(ref common.Re
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationEventFieldMapping(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Annotation event field mapping. Defines how to map a data frame field to an annotation event field.",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"source": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Source type for the field value",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"value": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Constant value to use when source is \"text\"",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"regex": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Regular expression to apply to the field value",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationPanelFilter(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
@@ -762,6 +797,21 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQuerySpec(ref common.
|
||||
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationPanelFilter"),
|
||||
},
|
||||
},
|
||||
"mappings": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Mappings define how to convert data frame fields to annotation event fields.",
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationEventFieldMapping"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"legacyOptions": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Catch-all field for datasource-specific properties",
|
||||
@@ -782,7 +832,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQuerySpec(ref common.
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationPanelFilter", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataSourceRef"},
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationEventFieldMapping", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationPanelFilter", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataSourceRef"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,10 +79,26 @@ LibraryPanelRef: {
|
||||
|
||||
AnnotationPanelFilter: {
|
||||
// Should the specified panels be included or excluded
|
||||
exclude?: bool | *false
|
||||
exclude?: bool | *false
|
||||
|
||||
// Panel IDs that should be included or excluded
|
||||
ids: [...uint32]
|
||||
}
|
||||
|
||||
// Annotation event field source. Defines how to obtain the value for an annotation event field.
|
||||
// - "field": Find the value with a matching key (default)
|
||||
// - "text": Write a constant string into the value
|
||||
// - "skip": Do not include the field
|
||||
AnnotationEventFieldSource: "field" | "text" | "skip" | *"field"
|
||||
|
||||
// Annotation event field mapping. Defines how to map a data frame field to an annotation event field.
|
||||
AnnotationEventFieldMapping: {
|
||||
// Source type for the field value
|
||||
source?: AnnotationEventFieldSource | *"field"
|
||||
// Constant value to use when source is "text"
|
||||
value?: string
|
||||
// Regular expression to apply to the field value
|
||||
regex?: string
|
||||
}
|
||||
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
@@ -454,7 +470,10 @@ AnnotationQuerySpec: {
|
||||
filter?: AnnotationPanelFilter
|
||||
// Placement can be used to display the annotation query somewhere else on the dashboard other than the default location.
|
||||
placement?: AnnotationQueryPlacement
|
||||
legacyOptions?: [string]: _ // Catch-all field for datasource-specific properties. Should not be available in as code tooling.
|
||||
// Mappings define how to convert data frame fields to annotation event fields.
|
||||
mappings?: [string]: AnnotationEventFieldMapping
|
||||
// Catch-all field for datasource-specific properties. Should not be available in as code tooling.
|
||||
legacyOptions?: [string]: _
|
||||
}
|
||||
|
||||
AnnotationQueryKind: {
|
||||
|
||||
@@ -32,6 +32,8 @@ type DashboardAnnotationQuerySpec struct {
|
||||
Filter *DashboardAnnotationPanelFilter `json:"filter,omitempty"`
|
||||
// Placement can be used to display the annotation query somewhere else on the dashboard other than the default location.
|
||||
Placement *string `json:"placement,omitempty"`
|
||||
// Mappings define how to convert data frame fields to annotation event fields.
|
||||
Mappings map[string]DashboardAnnotationEventFieldMapping `json:"mappings,omitempty"`
|
||||
// Catch-all field for datasource-specific properties. Should not be available in as code tooling.
|
||||
LegacyOptions map[string]interface{} `json:"legacyOptions,omitempty"`
|
||||
}
|
||||
@@ -86,6 +88,24 @@ func NewDashboardAnnotationPanelFilter() *DashboardAnnotationPanelFilter {
|
||||
// +k8s:openapi-gen=true
|
||||
const DashboardAnnotationQueryPlacement = "inControlsMenu"
|
||||
|
||||
// Annotation event field mapping. Defines how to map a data frame field to an annotation event field.
|
||||
// +k8s:openapi-gen=true
|
||||
type DashboardAnnotationEventFieldMapping struct {
|
||||
// Source type for the field value
|
||||
Source *string `json:"source,omitempty"`
|
||||
// Constant value to use when source is "text"
|
||||
Value *string `json:"value,omitempty"`
|
||||
// Regular expression to apply to the field value
|
||||
Regex *string `json:"regex,omitempty"`
|
||||
}
|
||||
|
||||
// NewDashboardAnnotationEventFieldMapping creates a new DashboardAnnotationEventFieldMapping object.
|
||||
func NewDashboardAnnotationEventFieldMapping() *DashboardAnnotationEventFieldMapping {
|
||||
return &DashboardAnnotationEventFieldMapping{
|
||||
Source: (func(input string) *string { return &input })("field"),
|
||||
}
|
||||
}
|
||||
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
// "Crosshair" for shared crosshair.
|
||||
// "Tooltip" for shared crosshair AND shared tooltip.
|
||||
|
||||
@@ -23,6 +23,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAdHocFilterWithLabels": schema_pkg_apis_dashboard_v2beta1_DashboardAdHocFilterWithLabels(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAdhocVariableKind": schema_pkg_apis_dashboard_v2beta1_DashboardAdhocVariableKind(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAdhocVariableSpec": schema_pkg_apis_dashboard_v2beta1_DashboardAdhocVariableSpec(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationEventFieldMapping": schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationEventFieldMapping(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationPanelFilter": schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationPanelFilter(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationQueryKind": schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationQueryKind(ref),
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationQuerySpec": schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationQuerySpec(ref),
|
||||
@@ -653,6 +654,40 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardAdhocVariableSpec(ref common.Ref
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationEventFieldMapping(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Annotation event field mapping. Defines how to map a data frame field to an annotation event field.",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"source": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Source type for the field value",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"value": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Constant value to use when source is \"text\"",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"regex": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Regular expression to apply to the field value",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationPanelFilter(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
@@ -774,6 +809,21 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationQuerySpec(ref common.R
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"mappings": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Mappings define how to convert data frame fields to annotation event fields.",
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationEventFieldMapping"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"legacyOptions": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Catch-all field for datasource-specific properties. Should not be available in as code tooling.",
|
||||
@@ -794,7 +844,7 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationQuerySpec(ref common.R
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationPanelFilter", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDataQueryKind"},
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationEventFieldMapping", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationPanelFilter", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardDataQueryKind"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-5
@@ -154,10 +154,22 @@
|
||||
"exclude": true
|
||||
},
|
||||
"mappings": {
|
||||
"title": "service",
|
||||
"text": "description",
|
||||
"time": "timestamp",
|
||||
"tags": "labels"
|
||||
"title": {
|
||||
"source": "field",
|
||||
"value": "service"
|
||||
},
|
||||
"text": {
|
||||
"source": "field",
|
||||
"value": "description"
|
||||
},
|
||||
"time": {
|
||||
"source": "field",
|
||||
"value": "timestamp"
|
||||
},
|
||||
"tags": {
|
||||
"source": "field",
|
||||
"value": "labels"
|
||||
}
|
||||
},
|
||||
"builtIn": 0,
|
||||
"type": "influxdb"
|
||||
@@ -214,4 +226,4 @@
|
||||
},
|
||||
"links": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v0alpha1.json
Vendored
+17
-5
@@ -151,10 +151,22 @@
|
||||
"hide": false,
|
||||
"iconColor": "#FF5722",
|
||||
"mappings": {
|
||||
"tags": "labels",
|
||||
"text": "description",
|
||||
"time": "timestamp",
|
||||
"title": "service"
|
||||
"tags": {
|
||||
"source": "field",
|
||||
"value": "labels"
|
||||
},
|
||||
"text": {
|
||||
"source": "field",
|
||||
"value": "description"
|
||||
},
|
||||
"time": {
|
||||
"source": "field",
|
||||
"value": "timestamp"
|
||||
},
|
||||
"title": {
|
||||
"source": "field",
|
||||
"value": "service"
|
||||
}
|
||||
},
|
||||
"name": "Complex Filter Annotation",
|
||||
"target": {
|
||||
@@ -227,4 +239,4 @@
|
||||
"storedVersion": "v1beta1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2alpha1.json
Vendored
+18
-6
@@ -215,13 +215,25 @@
|
||||
3
|
||||
]
|
||||
},
|
||||
"legacyOptions": {
|
||||
"mappings": {
|
||||
"tags": "labels",
|
||||
"text": "description",
|
||||
"time": "timestamp",
|
||||
"title": "service"
|
||||
"mappings": {
|
||||
"tags": {
|
||||
"source": "field",
|
||||
"value": "labels"
|
||||
},
|
||||
"text": {
|
||||
"source": "field",
|
||||
"value": "description"
|
||||
},
|
||||
"time": {
|
||||
"source": "field",
|
||||
"value": "timestamp"
|
||||
},
|
||||
"title": {
|
||||
"source": "field",
|
||||
"value": "service"
|
||||
}
|
||||
},
|
||||
"legacyOptions": {
|
||||
"type": "influxdb"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+18
-6
@@ -224,13 +224,25 @@
|
||||
3
|
||||
]
|
||||
},
|
||||
"legacyOptions": {
|
||||
"mappings": {
|
||||
"tags": "labels",
|
||||
"text": "description",
|
||||
"time": "timestamp",
|
||||
"title": "service"
|
||||
"mappings": {
|
||||
"tags": {
|
||||
"source": "field",
|
||||
"value": "labels"
|
||||
},
|
||||
"text": {
|
||||
"source": "field",
|
||||
"value": "description"
|
||||
},
|
||||
"time": {
|
||||
"source": "field",
|
||||
"value": "timestamp"
|
||||
},
|
||||
"title": {
|
||||
"source": "field",
|
||||
"value": "service"
|
||||
}
|
||||
},
|
||||
"legacyOptions": {
|
||||
"type": "influxdb"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1784,6 +1784,12 @@ func buildAnnotationQuery(annotationMap map[string]interface{}) (dashv2alpha1.Da
|
||||
filter = buildAnnotationFilter(filterMap)
|
||||
}
|
||||
|
||||
// Transform mappings
|
||||
var mappings map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping
|
||||
if mappingsMap, ok := annotationMap["mappings"].(map[string]interface{}); ok && mappingsMap != nil {
|
||||
mappings = convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
}
|
||||
|
||||
// Transform builtIn from float64 to bool
|
||||
var builtInPtr *bool
|
||||
if builtInVal, ok := annotationMap["builtIn"]; ok && builtInVal != nil {
|
||||
@@ -1809,6 +1815,7 @@ func buildAnnotationQuery(annotationMap map[string]interface{}) (dashv2alpha1.Da
|
||||
IconColor: schemaversion.GetStringValue(annotationMap, "iconColor", defaultAnnotationQuerySpec.IconColor),
|
||||
BuiltIn: builtInPtr,
|
||||
Filter: filter,
|
||||
Mappings: mappings,
|
||||
}
|
||||
|
||||
// Handle any additional properties in LegacyOptions
|
||||
@@ -1820,7 +1827,7 @@ func buildAnnotationQuery(annotationMap map[string]interface{}) (dashv2alpha1.Da
|
||||
// Add other legacy fields if they exist
|
||||
for key, value := range annotationMap {
|
||||
switch key {
|
||||
case "name", "datasource", "enable", "hide", "iconColor", "filter", "target", "builtIn", "type":
|
||||
case "name", "datasource", "enable", "hide", "iconColor", "filter", "target", "builtIn", "type", "mappings":
|
||||
// Skip already handled fields
|
||||
default:
|
||||
legacyOptions[key] = value
|
||||
@@ -1866,6 +1873,52 @@ func buildAnnotationFilter(filterMap map[string]interface{}) *dashv2alpha1.Dashb
|
||||
return filter
|
||||
}
|
||||
|
||||
func convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap map[string]interface{}) map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping {
|
||||
mappings := make(map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping)
|
||||
|
||||
for key, value := range mappingsMap {
|
||||
mapping := dashv2alpha1.DashboardAnnotationEventFieldMapping{}
|
||||
|
||||
// Handle simple string format (v1beta1 legacy format: "fieldName": "targetFieldName")
|
||||
if valueStr, ok := value.(string); ok && valueStr != "" {
|
||||
// Simple string mapping: treat as field source with the value as the field name
|
||||
defaultSource := "field"
|
||||
mapping.Source = &defaultSource
|
||||
mapping.Value = &valueStr
|
||||
mappings[key] = mapping
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle object format (v2alpha1 format: "fieldName": {"source": "field", "value": "...", "regex": "..."})
|
||||
mappingMap, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract source (defaults to "field" if not specified)
|
||||
if source, ok := mappingMap["source"].(string); ok && source != "" {
|
||||
mapping.Source = &source
|
||||
} else {
|
||||
defaultSource := "field"
|
||||
mapping.Source = &defaultSource
|
||||
}
|
||||
|
||||
// Extract value (optional)
|
||||
if valueStr, ok := mappingMap["value"].(string); ok && valueStr != "" {
|
||||
mapping.Value = &valueStr
|
||||
}
|
||||
|
||||
// Extract regex (optional)
|
||||
if regex, ok := mappingMap["regex"].(string); ok && regex != "" {
|
||||
mapping.Regex = ®ex
|
||||
}
|
||||
|
||||
mappings[key] = mapping
|
||||
}
|
||||
|
||||
return mappings
|
||||
}
|
||||
|
||||
// Panel helper functions
|
||||
|
||||
func transformPanelQueries(ctx context.Context, panelMap map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) []dashv2alpha1.DashboardPanelQueryKind {
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
package conversion
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConvertAnnotationMappings_V1beta1_to_V2alpha1(t *testing.T) {
|
||||
t.Run("should convert mappings with all fields", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "service",
|
||||
"regex": "",
|
||||
},
|
||||
"text": map[string]interface{}{
|
||||
"source": "text",
|
||||
"value": "constant text",
|
||||
},
|
||||
"time": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "timestamp",
|
||||
},
|
||||
"tags": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "labels",
|
||||
"regex": "/(.*)/",
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
require.Len(t, result, 4)
|
||||
|
||||
// Check title mapping
|
||||
titleMapping, ok := result["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
assert.Equal(t, "service", *titleMapping.Value)
|
||||
assert.Nil(t, titleMapping.Regex)
|
||||
|
||||
// Check text mapping
|
||||
textMapping, ok := result["text"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "text", *textMapping.Source)
|
||||
assert.Equal(t, "constant text", *textMapping.Value)
|
||||
assert.Nil(t, textMapping.Regex)
|
||||
|
||||
// Check time mapping
|
||||
timeMapping, ok := result["time"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *timeMapping.Source)
|
||||
assert.Equal(t, "timestamp", *timeMapping.Value)
|
||||
assert.Nil(t, timeMapping.Regex)
|
||||
|
||||
// Check tags mapping
|
||||
tagsMapping, ok := result["tags"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *tagsMapping.Source)
|
||||
assert.Equal(t, "labels", *tagsMapping.Value)
|
||||
assert.Equal(t, "/(.*)/", *tagsMapping.Regex)
|
||||
})
|
||||
|
||||
t.Run("should default source to field when not specified", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"value": "service",
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
titleMapping, ok := result["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
assert.Equal(t, "service", *titleMapping.Value)
|
||||
})
|
||||
|
||||
t.Run("should handle empty source string by defaulting to field", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "",
|
||||
"value": "service",
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
titleMapping, ok := result["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
})
|
||||
|
||||
t.Run("should handle skip source", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "skip",
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
titleMapping, ok := result["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "skip", *titleMapping.Source)
|
||||
assert.Nil(t, titleMapping.Value)
|
||||
assert.Nil(t, titleMapping.Regex)
|
||||
})
|
||||
|
||||
t.Run("should skip invalid mapping entries", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "service",
|
||||
},
|
||||
"invalid": 123, // Invalid: not a string or map
|
||||
"text": map[string]interface{}{
|
||||
"source": "text",
|
||||
"value": "constant",
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
// Should have 2 valid mappings (title and text)
|
||||
// String values are now treated as valid legacy format mappings
|
||||
require.Len(t, result, 2)
|
||||
_, ok := result["title"]
|
||||
require.True(t, ok)
|
||||
_, ok = result["text"]
|
||||
require.True(t, ok)
|
||||
_, ok = result["invalid"]
|
||||
assert.False(t, ok, "invalid entry should be skipped")
|
||||
})
|
||||
|
||||
t.Run("should handle empty mappings map", func(t *testing.T) {
|
||||
mappingsMap := map[string]interface{}{}
|
||||
|
||||
result := convertAnnotationMappings_V1beta1_to_V2alpha1(mappingsMap)
|
||||
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildAnnotationQuery_Mappings(t *testing.T) {
|
||||
t.Run("should extract mappings to top-level property", func(t *testing.T) {
|
||||
annotationMap := map[string]interface{}{
|
||||
"name": "Test Annotation",
|
||||
"enable": true,
|
||||
"hide": false,
|
||||
"iconColor": "red",
|
||||
"datasource": map[string]interface{}{
|
||||
"type": "prometheus",
|
||||
"uid": "test-uid",
|
||||
},
|
||||
"target": map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
"mappings": map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "service",
|
||||
},
|
||||
"text": map[string]interface{}{
|
||||
"source": "text",
|
||||
"value": "constant text",
|
||||
},
|
||||
"time": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "timestamp",
|
||||
"regex": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := buildAnnotationQuery(annotationMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify mappings are in the correct location
|
||||
require.NotNil(t, result.Spec.Mappings)
|
||||
assert.Len(t, result.Spec.Mappings, 3)
|
||||
|
||||
// Verify mappings content
|
||||
titleMapping, ok := result.Spec.Mappings["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
assert.Equal(t, "service", *titleMapping.Value)
|
||||
|
||||
textMapping, ok := result.Spec.Mappings["text"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "text", *textMapping.Source)
|
||||
assert.Equal(t, "constant text", *textMapping.Value)
|
||||
|
||||
// Verify mappings are NOT in legacyOptions
|
||||
if result.Spec.LegacyOptions != nil {
|
||||
_, hasMappingsInLegacy := result.Spec.LegacyOptions["mappings"]
|
||||
assert.False(t, hasMappingsInLegacy, "mappings should not be in legacyOptions")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should handle annotation without mappings", func(t *testing.T) {
|
||||
annotationMap := map[string]interface{}{
|
||||
"name": "Test Annotation",
|
||||
"enable": true,
|
||||
"hide": false,
|
||||
"iconColor": "red",
|
||||
"datasource": map[string]interface{}{
|
||||
"type": "prometheus",
|
||||
"uid": "test-uid",
|
||||
},
|
||||
"target": map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := buildAnnotationQuery(annotationMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Mappings should be nil when not present
|
||||
assert.Nil(t, result.Spec.Mappings)
|
||||
})
|
||||
|
||||
t.Run("should handle empty mappings", func(t *testing.T) {
|
||||
annotationMap := map[string]interface{}{
|
||||
"name": "Test Annotation",
|
||||
"enable": true,
|
||||
"hide": false,
|
||||
"iconColor": "red",
|
||||
"datasource": map[string]interface{}{
|
||||
"type": "prometheus",
|
||||
"uid": "test-uid",
|
||||
},
|
||||
"target": map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
"mappings": map[string]interface{}{},
|
||||
}
|
||||
|
||||
result, err := buildAnnotationQuery(annotationMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Empty mappings should result in empty map
|
||||
assert.NotNil(t, result.Spec.Mappings)
|
||||
assert.Empty(t, result.Spec.Mappings)
|
||||
})
|
||||
|
||||
t.Run("should exclude mappings from legacyOptions", func(t *testing.T) {
|
||||
annotationMap := map[string]interface{}{
|
||||
"name": "Test Annotation",
|
||||
"enable": true,
|
||||
"hide": false,
|
||||
"iconColor": "red",
|
||||
"datasource": map[string]interface{}{
|
||||
"type": "prometheus",
|
||||
"uid": "test-uid",
|
||||
},
|
||||
"target": map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
"mappings": map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "service",
|
||||
},
|
||||
},
|
||||
"type": "prometheus",
|
||||
"customField": "customValue",
|
||||
}
|
||||
|
||||
result, err := buildAnnotationQuery(annotationMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify mappings are in the correct location
|
||||
require.NotNil(t, result.Spec.Mappings)
|
||||
assert.Len(t, result.Spec.Mappings, 1)
|
||||
|
||||
// Verify other fields are in legacyOptions
|
||||
require.NotNil(t, result.Spec.LegacyOptions)
|
||||
assert.Equal(t, "prometheus", result.Spec.LegacyOptions["type"])
|
||||
assert.Equal(t, "customValue", result.Spec.LegacyOptions["customField"])
|
||||
|
||||
// Verify mappings are NOT in legacyOptions
|
||||
_, hasMappingsInLegacy := result.Spec.LegacyOptions["mappings"]
|
||||
assert.False(t, hasMappingsInLegacy, "mappings should not be in legacyOptions")
|
||||
})
|
||||
|
||||
t.Run("should handle mappings with regex", func(t *testing.T) {
|
||||
annotationMap := map[string]interface{}{
|
||||
"name": "Test Annotation",
|
||||
"enable": true,
|
||||
"hide": false,
|
||||
"iconColor": "red",
|
||||
"datasource": map[string]interface{}{
|
||||
"type": "prometheus",
|
||||
"uid": "test-uid",
|
||||
},
|
||||
"target": map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
"mappings": map[string]interface{}{
|
||||
"tags": map[string]interface{}{
|
||||
"source": "field",
|
||||
"value": "labels",
|
||||
"regex": "/(.*)/",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := buildAnnotationQuery(annotationMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, result.Spec.Mappings)
|
||||
tagsMapping, ok := result.Spec.Mappings["tags"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *tagsMapping.Source)
|
||||
assert.Equal(t, "labels", *tagsMapping.Value)
|
||||
assert.Equal(t, "/(.*)/", *tagsMapping.Regex)
|
||||
})
|
||||
}
|
||||
@@ -1721,6 +1721,14 @@ func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryK
|
||||
}
|
||||
}
|
||||
|
||||
// Convert mappings from v2alpha1 format back to v1beta1 format
|
||||
if len(annotation.Spec.Mappings) > 0 {
|
||||
mappings := convertAnnotationMappings_V2alpha1_to_V1beta1(annotation.Spec.Mappings)
|
||||
if len(mappings) > 0 {
|
||||
annotationMap["mappings"] = mappings
|
||||
}
|
||||
}
|
||||
|
||||
// Copy legacy options
|
||||
// This is used to copy any unknown properties from the v1 at the root of the annotations that were not handled by the conversion.
|
||||
// When they are converted into V2 they are moved to legacyOptions. Now we move them back to the root of the annotation.
|
||||
@@ -1728,7 +1736,7 @@ func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryK
|
||||
for k, v := range annotation.Spec.LegacyOptions {
|
||||
// Skip fields already handled
|
||||
if k != "name" && k != "enable" && k != "hide" && k != "iconColor" &&
|
||||
k != "datasource" && k != "target" && k != "filter" && k != "builtIn" && k != "placement" {
|
||||
k != "datasource" && k != "target" && k != "filter" && k != "builtIn" && k != "placement" && k != "mappings" {
|
||||
annotationMap[k] = v
|
||||
}
|
||||
}
|
||||
@@ -1740,6 +1748,46 @@ func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryK
|
||||
return result
|
||||
}
|
||||
|
||||
// convertAnnotationMappings_V2alpha1_to_V1beta1 converts mappings from v2alpha1 structured format
|
||||
// back to v1beta1 format. v1beta1 supports both simple string format and structured format with source/value/regex.
|
||||
// v2alpha1 format: map[string]DashboardAnnotationEventFieldMapping with Source, Value, Regex
|
||||
// v1beta1 format: map[string]interface{} where values can be either:
|
||||
// - string (legacy simple format: "fieldName": "targetFieldName")
|
||||
// - object (structured format: "fieldName": {"source": "field", "value": "...", "regex": "..."})
|
||||
func convertAnnotationMappings_V2alpha1_to_V1beta1(mappings map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for key, mapping := range mappings {
|
||||
// Always convert to structured format with source and value fields
|
||||
mappingMap := make(map[string]interface{})
|
||||
|
||||
// Source defaults to "field" if not specified
|
||||
source := "field"
|
||||
if mapping.Source != nil {
|
||||
source = *mapping.Source
|
||||
}
|
||||
mappingMap["source"] = source
|
||||
|
||||
// Value is optional (required for "field" and "text" sources, but "skip" doesn't need it)
|
||||
if mapping.Value != nil && *mapping.Value != "" {
|
||||
mappingMap["value"] = *mapping.Value
|
||||
}
|
||||
|
||||
// Regex is optional
|
||||
if mapping.Regex != nil && *mapping.Regex != "" {
|
||||
mappingMap["regex"] = *mapping.Regex
|
||||
}
|
||||
|
||||
// Include the mapping if it has source (and value for non-skip sources)
|
||||
// Skip source doesn't require a value
|
||||
if source == "skip" || (mapping.Value != nil && *mapping.Value != "") {
|
||||
result[key] = mappingMap
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Enum transformation functions (reverse of v1→v2)
|
||||
func transformVariableHideFromEnum(hide dashv2alpha1.DashboardVariableHide) interface{} {
|
||||
switch hide {
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package conversion
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
)
|
||||
|
||||
func TestConvertAnnotationMappings_V2alpha1_to_V1beta1(t *testing.T) {
|
||||
t.Run("should convert simple field mappings to structured format with source and value", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("service"),
|
||||
},
|
||||
"text": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("description"),
|
||||
},
|
||||
"time": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("timestamp"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 3)
|
||||
// All mappings should be in structured format with source and value
|
||||
titleMapping, ok := result["title"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", titleMapping["source"])
|
||||
assert.Equal(t, "service", titleMapping["value"])
|
||||
|
||||
textMapping, ok := result["text"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", textMapping["source"])
|
||||
assert.Equal(t, "description", textMapping["value"])
|
||||
|
||||
timeMapping, ok := result["time"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", timeMapping["source"])
|
||||
assert.Equal(t, "timestamp", timeMapping["value"])
|
||||
})
|
||||
|
||||
t.Run("should convert mappings with default field source to structured format", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: nil, // nil defaults to "field"
|
||||
Value: ptr.To("service"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
titleMapping, ok := result["title"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", titleMapping["source"])
|
||||
assert.Equal(t, "service", titleMapping["value"])
|
||||
})
|
||||
|
||||
t.Run("should preserve complex mappings with regex as structured format", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"tags": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("labels"),
|
||||
Regex: ptr.To("/(.*)/"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
tagsMapping, ok := result["tags"].(map[string]interface{})
|
||||
require.True(t, ok, "tags mapping should be a map")
|
||||
assert.Equal(t, "field", tagsMapping["source"])
|
||||
assert.Equal(t, "labels", tagsMapping["value"])
|
||||
assert.Equal(t, "/(.*)/", tagsMapping["regex"])
|
||||
})
|
||||
|
||||
t.Run("should preserve mappings with non-field source as structured format", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"text": {
|
||||
Source: ptr.To("text"),
|
||||
Value: ptr.To("constant text"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
textMapping, ok := result["text"].(map[string]interface{})
|
||||
require.True(t, ok, "text mapping should be a map")
|
||||
assert.Equal(t, "text", textMapping["source"])
|
||||
assert.Equal(t, "constant text", textMapping["value"])
|
||||
})
|
||||
|
||||
t.Run("should preserve mappings with skip source as structured format", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: ptr.To("skip"),
|
||||
},
|
||||
"text": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("description"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 2)
|
||||
// Skip mapping should be preserved as structured format
|
||||
titleMapping, ok := result["title"].(map[string]interface{})
|
||||
require.True(t, ok, "skip mapping should be preserved as map")
|
||||
assert.Equal(t, "skip", titleMapping["source"])
|
||||
// Field mapping should be structured format with source and value
|
||||
textMapping, ok := result["text"].(map[string]interface{})
|
||||
require.True(t, ok, "field mapping should be structured format")
|
||||
assert.Equal(t, "field", textMapping["source"])
|
||||
assert.Equal(t, "description", textMapping["value"])
|
||||
})
|
||||
|
||||
t.Run("should skip mappings without value", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: ptr.To("field"),
|
||||
Value: nil,
|
||||
},
|
||||
"text": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To(""),
|
||||
},
|
||||
"time": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("timestamp"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 1)
|
||||
_, ok := result["title"]
|
||||
assert.False(t, ok, "mapping without value should be skipped")
|
||||
_, ok = result["text"]
|
||||
assert.False(t, ok, "mapping with empty value should be skipped")
|
||||
_, ok = result["time"]
|
||||
assert.True(t, ok, "mapping with value should be included")
|
||||
})
|
||||
|
||||
t.Run("should handle empty mappings map", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("should handle all mappings in structured format", func(t *testing.T) {
|
||||
mappings := map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("service"),
|
||||
},
|
||||
"tags": {
|
||||
Source: ptr.To("field"),
|
||||
Value: ptr.To("labels"),
|
||||
Regex: ptr.To("/(.*)/"),
|
||||
},
|
||||
"text": {
|
||||
Source: ptr.To("text"),
|
||||
Value: ptr.To("constant"),
|
||||
},
|
||||
}
|
||||
|
||||
result := convertAnnotationMappings_V2alpha1_to_V1beta1(mappings)
|
||||
|
||||
require.Len(t, result, 3)
|
||||
// All mappings should be in structured format
|
||||
titleMapping, ok := result["title"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", titleMapping["source"])
|
||||
assert.Equal(t, "service", titleMapping["value"])
|
||||
|
||||
tagsMapping, ok := result["tags"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", tagsMapping["source"])
|
||||
assert.Equal(t, "labels", tagsMapping["value"])
|
||||
assert.Equal(t, "/(.*)/", tagsMapping["regex"])
|
||||
|
||||
textMapping, ok := result["text"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "text", textMapping["source"])
|
||||
assert.Equal(t, "constant", textMapping["value"])
|
||||
})
|
||||
}
|
||||
@@ -113,6 +113,11 @@ func convertAnnotationQuery_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardAnnota
|
||||
out.Spec.Filter = (*dashv2beta1.DashboardAnnotationPanelFilter)(in.Spec.Filter)
|
||||
out.Spec.LegacyOptions = in.Spec.LegacyOptions
|
||||
|
||||
// Convert mappings
|
||||
if in.Spec.Mappings != nil {
|
||||
out.Spec.Mappings = convertAnnotationMappings_V2alpha1_to_V2beta1(in.Spec.Mappings)
|
||||
}
|
||||
|
||||
// Convert query - move datasource from annotation spec to query
|
||||
if err := convertDataQuery_V2alpha1_to_V2beta1(in.Spec.Query, &out.Spec.Query, in.Spec.Datasource, scope); err != nil {
|
||||
return err
|
||||
@@ -982,3 +987,18 @@ func convertRowLayout_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardGridLayoutKi
|
||||
func convertTabLayout_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, out *dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, scope conversion.Scope) error {
|
||||
return convertLayout_V2alpha1_to_V2beta1(in, out, scope)
|
||||
}
|
||||
|
||||
func convertAnnotationMappings_V2alpha1_to_V2beta1(in map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping) map[string]dashv2beta1.DashboardAnnotationEventFieldMapping {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]dashv2beta1.DashboardAnnotationEventFieldMapping, len(in))
|
||||
for key, mapping := range in {
|
||||
out[key] = dashv2beta1.DashboardAnnotationEventFieldMapping{
|
||||
Source: mapping.Source,
|
||||
Value: mapping.Value,
|
||||
Regex: mapping.Regex,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -115,6 +115,83 @@ func TestV2alpha1ToV2beta1(t *testing.T) {
|
||||
assert.True(t, variable.SwitchVariableKind.Spec.SkipUrlSync)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "annotation query with mappings",
|
||||
createV2alpha1: func() *dashv2alpha1.Dashboard {
|
||||
sourceField := "field"
|
||||
sourceText := "text"
|
||||
valueService := "service"
|
||||
valueConstant := "constant text"
|
||||
regexPattern := "/(.*)/"
|
||||
return &dashv2alpha1.Dashboard{
|
||||
Spec: dashv2alpha1.DashboardSpec{
|
||||
Title: "Test Dashboard",
|
||||
Annotations: []dashv2alpha1.DashboardAnnotationQueryKind{
|
||||
{
|
||||
Kind: "AnnotationQuery",
|
||||
Spec: dashv2alpha1.DashboardAnnotationQuerySpec{
|
||||
Name: "Test Annotation",
|
||||
Enable: true,
|
||||
Hide: false,
|
||||
IconColor: "red",
|
||||
Query: &dashv2alpha1.DashboardDataQueryKind{
|
||||
Kind: "prometheus",
|
||||
Spec: map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
},
|
||||
Mappings: map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: &sourceField,
|
||||
Value: &valueService,
|
||||
},
|
||||
"text": {
|
||||
Source: &sourceText,
|
||||
Value: &valueConstant,
|
||||
},
|
||||
"tags": {
|
||||
Source: &sourceField,
|
||||
Value: &valueService,
|
||||
Regex: ®exPattern,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
validateV2beta1: func(t *testing.T, v2beta1 *dashv2beta1.Dashboard) {
|
||||
require.Len(t, v2beta1.Spec.Annotations, 1)
|
||||
annotation := v2beta1.Spec.Annotations[0]
|
||||
assert.Equal(t, "Test Annotation", annotation.Spec.Name)
|
||||
|
||||
// Verify mappings are preserved
|
||||
require.NotNil(t, annotation.Spec.Mappings)
|
||||
assert.Len(t, annotation.Spec.Mappings, 3)
|
||||
|
||||
// Check title mapping
|
||||
titleMapping, ok := annotation.Spec.Mappings["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
assert.Equal(t, "service", *titleMapping.Value)
|
||||
assert.Nil(t, titleMapping.Regex)
|
||||
|
||||
// Check text mapping
|
||||
textMapping, ok := annotation.Spec.Mappings["text"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "text", *textMapping.Source)
|
||||
assert.Equal(t, "constant text", *textMapping.Value)
|
||||
assert.Nil(t, textMapping.Regex)
|
||||
|
||||
// Check tags mapping
|
||||
tagsMapping, ok := annotation.Spec.Mappings["tags"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *tagsMapping.Source)
|
||||
assert.Equal(t, "service", *tagsMapping.Value)
|
||||
assert.Equal(t, "/(.*)/", *tagsMapping.Regex)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -114,6 +114,11 @@ func convertAnnotationQuery_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardAnnotat
|
||||
out.Spec.Filter = (*dashv2alpha1.DashboardAnnotationPanelFilter)(in.Spec.Filter)
|
||||
out.Spec.LegacyOptions = in.Spec.LegacyOptions
|
||||
|
||||
// Convert mappings
|
||||
if in.Spec.Mappings != nil {
|
||||
out.Spec.Mappings = convertAnnotationMappings_V2beta1_to_V2alpha1(in.Spec.Mappings)
|
||||
}
|
||||
|
||||
// Convert query - move datasource from query back to annotation spec
|
||||
query, datasource, err := convertDataQuery_V2beta1_to_V2alpha1(&in.Spec.Query, scope)
|
||||
if err != nil {
|
||||
@@ -1023,3 +1028,18 @@ func convertRowLayout_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardGridLayoutKin
|
||||
func convertTabLayout_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, out *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, scope conversion.Scope) error {
|
||||
return convertLayout_V2beta1_to_V2alpha1(in, out, scope)
|
||||
}
|
||||
|
||||
func convertAnnotationMappings_V2beta1_to_V2alpha1(in map[string]dashv2beta1.DashboardAnnotationEventFieldMapping) map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]dashv2alpha1.DashboardAnnotationEventFieldMapping, len(in))
|
||||
for key, mapping := range in {
|
||||
out[key] = dashv2alpha1.DashboardAnnotationEventFieldMapping{
|
||||
Source: mapping.Source,
|
||||
Value: mapping.Value,
|
||||
Regex: mapping.Regex,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -586,6 +586,85 @@ func TestV2beta1ToV2alpha1(t *testing.T) {
|
||||
assert.False(t, *row.Spec.Collapse)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "annotation query with mappings",
|
||||
createV2beta1: func() *dashv2beta1.Dashboard {
|
||||
sourceField := "field"
|
||||
sourceText := "text"
|
||||
valueService := "service"
|
||||
valueConstant := "constant text"
|
||||
regexPattern := "/(.*)/"
|
||||
return &dashv2beta1.Dashboard{
|
||||
Spec: dashv2beta1.DashboardSpec{
|
||||
Title: "Test Dashboard",
|
||||
Annotations: []dashv2beta1.DashboardAnnotationQueryKind{
|
||||
{
|
||||
Kind: "AnnotationQuery",
|
||||
Spec: dashv2beta1.DashboardAnnotationQuerySpec{
|
||||
Name: "Test Annotation",
|
||||
Enable: true,
|
||||
Hide: false,
|
||||
IconColor: "red",
|
||||
Query: dashv2beta1.DashboardDataQueryKind{
|
||||
Kind: "DataQuery",
|
||||
Group: "prometheus",
|
||||
Version: "v0",
|
||||
Spec: map[string]interface{}{
|
||||
"expr": "test_query",
|
||||
},
|
||||
},
|
||||
Mappings: map[string]dashv2beta1.DashboardAnnotationEventFieldMapping{
|
||||
"title": {
|
||||
Source: &sourceField,
|
||||
Value: &valueService,
|
||||
},
|
||||
"text": {
|
||||
Source: &sourceText,
|
||||
Value: &valueConstant,
|
||||
},
|
||||
"tags": {
|
||||
Source: &sourceField,
|
||||
Value: &valueService,
|
||||
Regex: ®exPattern,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) {
|
||||
require.Len(t, v2alpha1.Spec.Annotations, 1)
|
||||
annotation := v2alpha1.Spec.Annotations[0]
|
||||
assert.Equal(t, "Test Annotation", annotation.Spec.Name)
|
||||
|
||||
// Verify mappings are preserved
|
||||
require.NotNil(t, annotation.Spec.Mappings)
|
||||
assert.Len(t, annotation.Spec.Mappings, 3)
|
||||
|
||||
// Check title mapping
|
||||
titleMapping, ok := annotation.Spec.Mappings["title"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *titleMapping.Source)
|
||||
assert.Equal(t, "service", *titleMapping.Value)
|
||||
assert.Nil(t, titleMapping.Regex)
|
||||
|
||||
// Check text mapping
|
||||
textMapping, ok := annotation.Spec.Mappings["text"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "text", *textMapping.Source)
|
||||
assert.Equal(t, "constant text", *textMapping.Value)
|
||||
assert.Nil(t, textMapping.Regex)
|
||||
|
||||
// Check tags mapping
|
||||
tagsMapping, ok := annotation.Spec.Mappings["tags"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "field", *tagsMapping.Source)
|
||||
assert.Equal(t, "service", *tagsMapping.Value)
|
||||
assert.Equal(t, "/(.*)/", *tagsMapping.Regex)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.25.3
|
||||
FROM golang:1.25.5
|
||||
|
||||
ADD main.go /go/src/webhook/main.go
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface AnnotationQuerySpec {
|
||||
name: string;
|
||||
builtIn?: boolean;
|
||||
filter?: AnnotationPanelFilter;
|
||||
// Mappings define how to convert data frame fields to annotation event fields.
|
||||
mappings?: Record<string, AnnotationEventFieldMapping>;
|
||||
// Catch-all field for datasource-specific properties
|
||||
legacyOptions?: Record<string, any>;
|
||||
}
|
||||
@@ -64,6 +66,20 @@ export const defaultAnnotationPanelFilter = (): AnnotationPanelFilter => ({
|
||||
ids: [],
|
||||
});
|
||||
|
||||
// Annotation event field mapping. Defines how to map a data frame field to an annotation event field.
|
||||
export interface AnnotationEventFieldMapping {
|
||||
// Source type for the field value
|
||||
source?: string;
|
||||
// Constant value to use when source is "text"
|
||||
value?: string;
|
||||
// Regular expression to apply to the field value
|
||||
regex?: string;
|
||||
}
|
||||
|
||||
export const defaultAnnotationEventFieldMapping = (): AnnotationEventFieldMapping => ({
|
||||
source: "field",
|
||||
});
|
||||
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
// "Crosshair" for shared crosshair.
|
||||
// "Tooltip" for shared crosshair AND shared tooltip.
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface AnnotationQuerySpec {
|
||||
filter?: AnnotationPanelFilter;
|
||||
// Placement can be used to display the annotation query somewhere else on the dashboard other than the default location.
|
||||
placement?: "inControlsMenu";
|
||||
// Mappings define how to convert data frame fields to annotation event fields.
|
||||
mappings?: Record<string, AnnotationEventFieldMapping>;
|
||||
// Catch-all field for datasource-specific properties. Should not be available in as code tooling.
|
||||
legacyOptions?: Record<string, any>;
|
||||
}
|
||||
@@ -69,6 +71,20 @@ export const defaultAnnotationPanelFilter = (): AnnotationPanelFilter => ({
|
||||
// - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu
|
||||
export const AnnotationQueryPlacement = "inControlsMenu";
|
||||
|
||||
// Annotation event field mapping. Defines how to map a data frame field to an annotation event field.
|
||||
export interface AnnotationEventFieldMapping {
|
||||
// Source type for the field value
|
||||
source?: string;
|
||||
// Constant value to use when source is "text"
|
||||
value?: string;
|
||||
// Regular expression to apply to the field value
|
||||
regex?: string;
|
||||
}
|
||||
|
||||
export const defaultAnnotationEventFieldMapping = (): AnnotationEventFieldMapping => ({
|
||||
source: "field",
|
||||
});
|
||||
|
||||
// "Off" for no shared crosshair or tooltip (default).
|
||||
// "Crosshair" for shared crosshair.
|
||||
// "Tooltip" for shared crosshair AND shared tooltip.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
|
||||
@@ -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) {
|
||||
</Button>
|
||||
);
|
||||
|
||||
// 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 = (
|
||||
<Button
|
||||
onClick={() => setShowBulkExportProvisionedResource(true)}
|
||||
variant="secondary"
|
||||
disabled={!hasUnmanaged || isLoadingUnmanaged || !hasSelectedDashboards}
|
||||
>
|
||||
<Trans i18nKey="browse-dashboards.action.export-to-repository-button">Export to Repository</Trans>
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap={1} data-testid="manage-actions">
|
||||
{moveButton}
|
||||
{provisioningEnabled && pushButton}
|
||||
|
||||
<Button onClick={showDeleteModal} variant="destructive">
|
||||
<Trans i18nKey="browse-dashboards.action.delete-button">Delete</Trans>
|
||||
@@ -191,7 +162,7 @@ export function BrowseActions({ folderDTO }: Props) {
|
||||
size="md"
|
||||
>
|
||||
<BulkDeleteProvisionedResource
|
||||
selectedItems={selectedItemsForActions}
|
||||
selectedItems={selectedItems}
|
||||
folderUid={folderDTO?.uid || ''}
|
||||
onDismiss={() => {
|
||||
setShowBulkDeleteProvisionedResource(false);
|
||||
@@ -213,32 +184,10 @@ export function BrowseActions({ folderDTO }: Props) {
|
||||
size="md"
|
||||
>
|
||||
<BulkMoveProvisionedResource
|
||||
selectedItems={selectedItemsForActions}
|
||||
folderUid={folderDTO?.uid}
|
||||
onDismiss={() => {
|
||||
setShowBulkMoveProvisionedResource(false);
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
)}
|
||||
|
||||
{/* bulk export */}
|
||||
{showBulkExportProvisionedResource && (
|
||||
<Drawer
|
||||
title={
|
||||
// Heading levels should only increase by one (a11y)
|
||||
<Text variant="h3" element="h2">
|
||||
{t('browse-dashboards.action.export-provisioned-resources', 'Export Resources')}
|
||||
</Text>
|
||||
}
|
||||
onClose={() => setShowBulkExportProvisionedResource(false)}
|
||||
size="md"
|
||||
>
|
||||
<BulkExportProvisionedResource
|
||||
selectedItems={selectedItems}
|
||||
folderUid={folderDTO?.uid}
|
||||
onDismiss={() => {
|
||||
setShowBulkExportProvisionedResource(false);
|
||||
setShowBulkMoveProvisionedResource(false);
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { DashboardViewItem } from 'app/features/search/types';
|
||||
import { useDispatch, useSelector } from 'app/types/store';
|
||||
|
||||
import { PAGE_SIZE } from '../api/services';
|
||||
import { fetchNextChildrenPage, selectFolderWithAllDashboards } from '../state/actions';
|
||||
import { fetchNextChildrenPage } from '../state/actions';
|
||||
import {
|
||||
useFlatTreeState,
|
||||
useCheckboxSelectionState,
|
||||
@@ -81,13 +81,7 @@ export function BrowseView({ folderUID, width, height, permissions, isReadOnlyRe
|
||||
|
||||
const handleItemSelectionChange = useCallback(
|
||||
(item: DashboardViewItem, isSelected: boolean) => {
|
||||
// If selecting a folder, use the async thunk to collect all dashboards recursively
|
||||
// When deselecting, the normal reducer will handle deselecting all children
|
||||
if (item.kind === 'folder') {
|
||||
dispatch(selectFolderWithAllDashboards({ folderUID: item.uid, isSelected }));
|
||||
} else {
|
||||
dispatch(setItemSelectionState({ item, isSelected }));
|
||||
}
|
||||
dispatch(setItemSelectionState({ item, isSelected }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
@@ -2,16 +2,14 @@ import { useState } from 'react';
|
||||
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { config, locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { Button, Drawer, Dropdown, Icon, Menu, MenuItem, Text } from '@grafana/ui';
|
||||
import { appEvents } from 'app/core/app_events';
|
||||
import { Permissions } from 'app/core/components/AccessControl/Permissions';
|
||||
import { RepoType } from 'app/features/provisioning/Wizard/types';
|
||||
import { BulkExportProvisionedResource } from 'app/features/provisioning/components/BulkActions/BulkExportProvisionedResource';
|
||||
import { BulkMoveProvisionedResource } from 'app/features/provisioning/components/BulkActions/BulkMoveProvisionedResource';
|
||||
import { DeleteProvisionedFolderForm } from 'app/features/provisioning/components/Folders/DeleteProvisionedFolderForm';
|
||||
import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance';
|
||||
import { collectAllDashboardsUnderFolder } from 'app/features/provisioning/utils/collectFolderDashboards';
|
||||
import { getReadOnlyTooltipText } from 'app/features/provisioning/utils/repository';
|
||||
import { ShowModalReactEvent } from 'app/types/events';
|
||||
import { FolderDTO } from 'app/types/folders';
|
||||
@@ -34,8 +32,6 @@ export function FolderActionsButton({ folder, repoType, isReadOnlyRepo }: Props)
|
||||
const [showPermissionsDrawer, setShowPermissionsDrawer] = useState(false);
|
||||
const [showDeleteProvisionedFolderDrawer, setShowDeleteProvisionedFolderDrawer] = useState(false);
|
||||
const [showMoveProvisionedFolderDrawer, setShowMoveProvisionedFolderDrawer] = useState(false);
|
||||
const [showExportFolderDrawer, setShowExportFolderDrawer] = useState(false);
|
||||
const [exportSelectedDashboards, setExportSelectedDashboards] = useState<Record<string, boolean>>({});
|
||||
const [moveFolder] = useMoveFolderMutationFacade();
|
||||
const isProvisionedInstance = useIsProvisionedInstance();
|
||||
|
||||
@@ -129,38 +125,9 @@ export function FolderActionsButton({ folder, repoType, isReadOnlyRepo }: Props)
|
||||
setShowMoveProvisionedFolderDrawer(true);
|
||||
};
|
||||
|
||||
const handleExportFolder = async () => {
|
||||
try {
|
||||
// Collect all dashboards under this folder and its children
|
||||
const dashboardUIDs = await collectAllDashboardsUnderFolder(folder.uid);
|
||||
|
||||
// Create selected items object with all dashboards
|
||||
const selectedDashboards: Record<string, boolean> = {};
|
||||
dashboardUIDs.forEach((uid) => {
|
||||
selectedDashboards[uid] = true;
|
||||
});
|
||||
|
||||
setExportSelectedDashboards(selectedDashboards);
|
||||
setShowExportFolderDrawer(true);
|
||||
} catch (error) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: [
|
||||
t(
|
||||
'browse-dashboards.folder-actions-button.export-folder-error',
|
||||
'Error collecting dashboards. Please try again later.'
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const managePermissionsLabel = t('browse-dashboards.folder-actions-button.manage-permissions', 'Manage permissions');
|
||||
const moveLabel = t('browse-dashboards.folder-actions-button.move', 'Move this folder');
|
||||
const deleteLabel = t('browse-dashboards.folder-actions-button.delete', 'Delete this folder');
|
||||
const exportLabel = t('browse-dashboards.folder-actions-button.export', 'Export to Repository');
|
||||
|
||||
const canExportToRepository = config.featureToggles.provisioning && !isProvisionedFolder;
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
@@ -180,18 +147,10 @@ export function FolderActionsButton({ folder, repoType, isReadOnlyRepo }: Props)
|
||||
label={deleteLabel}
|
||||
/>
|
||||
)}
|
||||
{canExportToRepository && <MenuItem onClick={handleExportFolder} label={exportLabel} />}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
// Show menu if there are any available actions
|
||||
const hasAnyActions =
|
||||
(canViewPermissions && !isProvisionedFolder) ||
|
||||
(canMoveFolder && !isReadOnlyRepo) ||
|
||||
(canDeleteFolders && !isReadOnlyRepo) ||
|
||||
canExportToRepository;
|
||||
|
||||
if (!hasAnyActions) {
|
||||
if (!canViewPermissions && !canMoveFolder && !canDeleteFolders) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -254,30 +213,6 @@ export function FolderActionsButton({ folder, repoType, isReadOnlyRepo }: Props)
|
||||
/>
|
||||
</Drawer>
|
||||
)}
|
||||
{showExportFolderDrawer && (
|
||||
<Drawer
|
||||
title={
|
||||
<Text variant="h3" element="h2">
|
||||
{t('browse-dashboards.action.export-folder', 'Export Folder to Repository')}
|
||||
</Text>
|
||||
}
|
||||
subtitle={folder.title}
|
||||
onClose={() => setShowExportFolderDrawer(false)}
|
||||
size="md"
|
||||
>
|
||||
<BulkExportProvisionedResource
|
||||
folderUid={folder.uid}
|
||||
selectedItems={{
|
||||
dashboard: exportSelectedDashboards,
|
||||
folder: {},
|
||||
}}
|
||||
onDismiss={() => {
|
||||
setShowExportFolderDrawer(false);
|
||||
setExportSelectedDashboards({});
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { createAsyncThunk } from 'app/types/store';
|
||||
import { listDashboards, listFolders, PAGE_SIZE } from '../api/services';
|
||||
import { DashboardViewItemWithUIItems, UIDashboardViewItem } from '../types';
|
||||
|
||||
import { setItemSelectionState } from './slice';
|
||||
import { findItem } from './utils';
|
||||
|
||||
interface FetchNextChildrenPageArgs {
|
||||
@@ -89,69 +88,6 @@ export const refetchChildren = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
export const selectFolderWithAllDashboards = createAsyncThunk(
|
||||
'browseDashboards/selectFolderWithAllDashboards',
|
||||
async ({ folderUID, isSelected }: { folderUID: string; isSelected: boolean }, { dispatch, getState }) => {
|
||||
const state = getState().browseDashboards;
|
||||
|
||||
// Find the folder item to get its parentUID and managedBy
|
||||
const folderItem = findItem(state.rootItems?.items ?? [], state.childrenByParentUID, folderUID);
|
||||
|
||||
if (!isSelected) {
|
||||
// When deselecting, use the normal action - it will handle deselecting all children recursively
|
||||
dispatch(
|
||||
setItemSelectionState({
|
||||
item: {
|
||||
kind: 'folder',
|
||||
uid: folderUID,
|
||||
parentUID: folderItem?.parentUID,
|
||||
managedBy: folderItem?.managedBy,
|
||||
},
|
||||
isSelected: false,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// When selecting, collect all dashboards recursively
|
||||
const { collectAllDashboardsUnderFolder } = await import('app/features/provisioning/utils/collectFolderDashboards');
|
||||
const dashboardUIDs = await collectAllDashboardsUnderFolder(folderUID);
|
||||
|
||||
// First, select the folder itself
|
||||
dispatch(
|
||||
setItemSelectionState({
|
||||
item: {
|
||||
kind: 'folder',
|
||||
uid: folderUID,
|
||||
parentUID: folderItem?.parentUID,
|
||||
managedBy: folderItem?.managedBy,
|
||||
},
|
||||
isSelected: true,
|
||||
})
|
||||
);
|
||||
|
||||
// Then select all dashboards found
|
||||
// We need to get the parentUID for each dashboard from the state
|
||||
// If a dashboard isn't in state yet, we still need to select it
|
||||
for (const dashboardUID of dashboardUIDs) {
|
||||
const dashboardItem = findItem(state.rootItems?.items ?? [], state.childrenByParentUID, dashboardUID);
|
||||
// Even if dashboard isn't in state, we can still select it by UID
|
||||
// The reducer will handle setting selectedItems.dashboard[dashboardUID] = true
|
||||
dispatch(
|
||||
setItemSelectionState({
|
||||
item: {
|
||||
kind: 'dashboard',
|
||||
uid: dashboardUID,
|
||||
parentUID: dashboardItem?.parentUID ?? folderUID, // Fallback to folderUID if not found
|
||||
managedBy: dashboardItem?.managedBy,
|
||||
},
|
||||
isSelected: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchNextChildrenPage = createAsyncThunk(
|
||||
'browseDashboards/fetchNextChildrenPage',
|
||||
async (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AnnotationQuery } from '@grafana/data';
|
||||
import { AnnotationQuery, AnnotationEventFieldSource } from '@grafana/data';
|
||||
import { AnnotationQueryKind } from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
|
||||
import { transformV1ToV2AnnotationQuery, transformV2ToV1AnnotationQuery } from './annotations';
|
||||
@@ -220,4 +220,145 @@ describe('V1<->V2 annotation convertions', () => {
|
||||
const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2);
|
||||
expect(resultV1).toEqual(annotationDefinition);
|
||||
});
|
||||
|
||||
test('given annotations with mappings', () => {
|
||||
const annotationDefinition: AnnotationQuery = {
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'uid',
|
||||
},
|
||||
enable: true,
|
||||
hide: false,
|
||||
iconColor: 'red',
|
||||
name: 'prom-annos',
|
||||
target: {
|
||||
// @ts-expect-error
|
||||
expr: '{action="add_client"}',
|
||||
refId: 'Anno',
|
||||
},
|
||||
mappings: {
|
||||
title: {
|
||||
source: AnnotationEventFieldSource.Field,
|
||||
value: 'service',
|
||||
},
|
||||
text: {
|
||||
source: AnnotationEventFieldSource.Text,
|
||||
value: 'constant text',
|
||||
},
|
||||
tags: {
|
||||
source: AnnotationEventFieldSource.Field,
|
||||
value: 'labels',
|
||||
regex: '/(.*)/',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const expectedV2: AnnotationQueryKind = {
|
||||
kind: 'AnnotationQuery',
|
||||
spec: {
|
||||
enable: true,
|
||||
hide: false,
|
||||
iconColor: 'red',
|
||||
name: 'prom-annos',
|
||||
query: {
|
||||
kind: 'DataQuery',
|
||||
group: 'prometheus',
|
||||
version: 'v0',
|
||||
datasource: {
|
||||
name: 'uid',
|
||||
},
|
||||
spec: {
|
||||
expr: '{action="add_client"}',
|
||||
refId: 'Anno',
|
||||
},
|
||||
},
|
||||
mappings: {
|
||||
title: {
|
||||
source: 'field',
|
||||
value: 'service',
|
||||
},
|
||||
text: {
|
||||
source: 'text',
|
||||
value: 'constant text',
|
||||
},
|
||||
tags: {
|
||||
source: 'field',
|
||||
value: 'labels',
|
||||
regex: '/(.*)/',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const resultV2: AnnotationQueryKind = transformV1ToV2AnnotationQuery(annotationDefinition, 'prometheus', 'uid');
|
||||
expect(resultV2).toEqual(expectedV2);
|
||||
|
||||
const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2);
|
||||
expect(resultV1).toEqual(annotationDefinition);
|
||||
});
|
||||
|
||||
test('given annotations with mappings and legacyOptions', () => {
|
||||
const annotationDefinition: AnnotationQuery = {
|
||||
datasource: {
|
||||
type: 'elasticsearch',
|
||||
uid: 'uid',
|
||||
},
|
||||
enable: true,
|
||||
hide: false,
|
||||
iconColor: 'red',
|
||||
name: 'elastic-annos',
|
||||
target: {
|
||||
// @ts-expect-error
|
||||
query: 'test query',
|
||||
refId: 'Anno',
|
||||
},
|
||||
mappings: {
|
||||
title: {
|
||||
source: AnnotationEventFieldSource.Field,
|
||||
value: 'service',
|
||||
},
|
||||
},
|
||||
// These should go to legacyOptions
|
||||
tagsField: 'asd',
|
||||
textField: 'asd',
|
||||
};
|
||||
|
||||
const expectedV2: AnnotationQueryKind = {
|
||||
kind: 'AnnotationQuery',
|
||||
spec: {
|
||||
enable: true,
|
||||
hide: false,
|
||||
iconColor: 'red',
|
||||
name: 'elastic-annos',
|
||||
query: {
|
||||
kind: 'DataQuery',
|
||||
group: 'elasticsearch',
|
||||
version: 'v0',
|
||||
datasource: {
|
||||
name: 'uid',
|
||||
},
|
||||
spec: {
|
||||
query: 'test query',
|
||||
refId: 'Anno',
|
||||
},
|
||||
},
|
||||
mappings: {
|
||||
title: {
|
||||
source: 'field',
|
||||
value: 'service',
|
||||
},
|
||||
},
|
||||
legacyOptions: {
|
||||
tagsField: 'asd',
|
||||
textField: 'asd',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const resultV2: AnnotationQueryKind = transformV1ToV2AnnotationQuery(annotationDefinition, 'elasticsearch', 'uid');
|
||||
expect(resultV2).toEqual(expectedV2);
|
||||
|
||||
const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2);
|
||||
expect(resultV1).toEqual(annotationDefinition);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,7 +74,10 @@ export function transformV1ToV2AnnotationQuery(
|
||||
result.spec.filter = annotation.filter;
|
||||
}
|
||||
|
||||
// TODO: add mappings
|
||||
// Add mappings if they exist
|
||||
if (annotation.mappings && Object.keys(annotation.mappings).length > 0) {
|
||||
result.spec.mappings = annotation.mappings;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -88,9 +91,13 @@ export function transformV2ToV1AnnotationQuery(annotation: AnnotationQueryKind):
|
||||
hide: annotation.spec.hide,
|
||||
iconColor: annotation.spec.iconColor,
|
||||
name: annotation.spec.name,
|
||||
// TOOO: mappings
|
||||
};
|
||||
|
||||
// Add mappings if they exist
|
||||
if (annotation.spec.mappings && Object.keys(annotation.spec.mappings).length > 0) {
|
||||
annoQuerySpec.mappings = annotation.spec.mappings;
|
||||
}
|
||||
|
||||
// Add placement if it exists
|
||||
if (annotation.spec.placement) {
|
||||
annoQuerySpec.placement = annotation.spec.placement;
|
||||
|
||||
@@ -80,8 +80,9 @@ export function DeleteDashboardModal({ dashboardTitle, onConfirm, onClose }: Del
|
||||
<>
|
||||
<Text element="p">
|
||||
<Trans i18nKey="dashboard-settings.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.
|
||||
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.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Space v={1} />
|
||||
|
||||
@@ -28,9 +28,6 @@ export function addDashboardExportDrawerItem(item: ExportDrawerMenuItem) {
|
||||
}
|
||||
|
||||
export default function ExportMenu({ dashboard }: { dashboard: DashboardScene }) {
|
||||
const provisioningEnabled = config.featureToggles.provisioning;
|
||||
const isUnmanaged = provisioningEnabled && !dashboard.isManagedRepository();
|
||||
|
||||
const onMenuItemClick = (shareView: string) => {
|
||||
locationService.partial({ shareView });
|
||||
};
|
||||
@@ -62,20 +59,8 @@ export default function ExportMenu({ dashboard }: { dashboard: DashboardScene })
|
||||
onClick: () => onMenuItemClick(shareDashboardType.image),
|
||||
});
|
||||
|
||||
// Add "Export to Repository" option for unmanaged dashboards
|
||||
if (isUnmanaged) {
|
||||
menuItems.push({
|
||||
shareId: 'export-to-repository',
|
||||
testId: 'export-to-repository',
|
||||
icon: 'cloud-upload',
|
||||
label: t('share-dashboard.menu.export-to-repository-title', 'Export to Repository'),
|
||||
renderCondition: true,
|
||||
onClick: () => onMenuItemClick('export-to-repository'),
|
||||
});
|
||||
}
|
||||
|
||||
return menuItems.filter((item) => item.renderCondition);
|
||||
}, [isUnmanaged]);
|
||||
}, []);
|
||||
|
||||
const onClick = (item: ExportDrawerMenuItem) => {
|
||||
DashboardInteractions.sharingCategoryClicked({
|
||||
|
||||
@@ -7,7 +7,6 @@ import { DashboardScene } from '../../scene/DashboardScene';
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
import { ExportAsCode } from '../ExportButton/ExportAsCode';
|
||||
import { ExportAsImage } from '../ExportButton/ExportAsImage';
|
||||
import { ExportToRepository } from '../ExportButton/ExportToRepository';
|
||||
import { ShareExternally } from '../ShareButton/share-externally/ShareExternally';
|
||||
import { ShareInternally } from '../ShareButton/share-internally/ShareInternally';
|
||||
import { ShareSnapshot } from '../ShareButton/share-snapshot/ShareSnapshot';
|
||||
@@ -97,8 +96,6 @@ function getShareView(
|
||||
return new ExportAsCode({ onDismiss });
|
||||
case shareDashboardType.image:
|
||||
return new ExportAsImage({ onDismiss });
|
||||
case 'export-to-repository':
|
||||
return new ExportToRepository({ onDismiss });
|
||||
default:
|
||||
return new ShareInternally({ onDismiss });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { MetricFindValue, TypedVariableModel } from '@grafana/data';
|
||||
import { MetricFindValue, TypedVariableModel, AnnotationQuery } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
AnnotationQuery,
|
||||
DataQuery,
|
||||
DataSourceRef,
|
||||
Panel,
|
||||
@@ -845,7 +844,7 @@ function getVariables(vars: TypedVariableModel[]): DashboardV2Spec['variables']
|
||||
function getAnnotations(annotations: AnnotationQuery[]): DashboardV2Spec['annotations'] {
|
||||
return annotations.map((a) => {
|
||||
// Extract properties that are explicitly handled
|
||||
const { name, enable, hide, iconColor, builtIn, datasource, target, filter, ...legacyOptions } = a;
|
||||
const { name, enable, hide, iconColor, builtIn, datasource, target, filter, mappings, ...legacyOptions } = a;
|
||||
|
||||
const aq: AnnotationQueryKind = {
|
||||
kind: 'AnnotationQuery',
|
||||
@@ -855,6 +854,7 @@ function getAnnotations(annotations: AnnotationQuery[]): DashboardV2Spec['annota
|
||||
hide: Boolean(hide),
|
||||
iconColor: iconColor,
|
||||
builtIn: Boolean(builtIn),
|
||||
...(mappings && { mappings: transformAnnotationMappingsV1ToV2(mappings) }),
|
||||
query: {
|
||||
kind: 'DataQuery',
|
||||
version: defaultDataQueryKind().version,
|
||||
@@ -1349,3 +1349,25 @@ export function transformDashboardV2SpecToV1(spec: DashboardV2Spec, metadata: Ob
|
||||
templating: { list: variables },
|
||||
};
|
||||
}
|
||||
|
||||
export function transformAnnotationMappingsV1ToV2(
|
||||
mappings: AnnotationQuery['mappings']
|
||||
): AnnotationQueryKind['spec']['mappings'] {
|
||||
if (!mappings) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(mappings).map(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
return [key, { source: 'field', value }];
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return [key, value.source ? value : { source: 'field', ...value }];
|
||||
}
|
||||
|
||||
return [key, value];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,11 @@ import { saveAs } from 'file-saver';
|
||||
import { memo, useState, useMemo } from 'react';
|
||||
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Button, Drawer, Field, Modal, Switch, Text } from '@grafana/ui';
|
||||
import { Button, Field, Modal, Switch } from '@grafana/ui';
|
||||
import { appEvents } from 'app/core/app_events';
|
||||
import { DashboardExporter } from 'app/features/dashboard/components/DashExportModal/DashboardExporter';
|
||||
import { makeExportableV1 } from 'app/features/dashboard-scene/scene/export/exporters';
|
||||
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
|
||||
import { BulkExportProvisionedResource } from 'app/features/provisioning/components/BulkActions/BulkExportProvisionedResource';
|
||||
import { ShowModalReactEvent } from 'app/types/events';
|
||||
|
||||
import { ViewJsonModal } from './ViewJsonModal';
|
||||
@@ -19,10 +17,7 @@ interface Props extends ShareModalTabProps {}
|
||||
|
||||
export const ShareExport = memo(({ dashboard, panel, onDismiss }: Props) => {
|
||||
const [shareExternally, setShareExternally] = useState(false);
|
||||
const [showExportToRepositoryDrawer, setShowExportToRepositoryDrawer] = useState(false);
|
||||
const exporter = useMemo(() => new DashboardExporter(), []);
|
||||
const provisioningEnabled = config.featureToggles.provisioning;
|
||||
const isUnmanaged = !dashboard.meta.provisioned;
|
||||
|
||||
const onShareExternallyChange = () => setShareExternally((prev) => !prev);
|
||||
|
||||
@@ -34,10 +29,6 @@ export const ShareExport = memo(({ dashboard, panel, onDismiss }: Props) => {
|
||||
|
||||
if (shareExternally) {
|
||||
makeExportableV1(dashboard).then((dashboardJson) => {
|
||||
if ('error' in dashboardJson) {
|
||||
console.error('Failed to export dashboard:', dashboardJson.error);
|
||||
return;
|
||||
}
|
||||
openSaveAsDialog(dashboardJson);
|
||||
});
|
||||
} else {
|
||||
@@ -60,17 +51,13 @@ export const ShareExport = memo(({ dashboard, panel, onDismiss }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const openSaveAsDialog = (dash: unknown) => {
|
||||
const openSaveAsDialog = (dash: any) => {
|
||||
const dashboardJsonPretty = JSON.stringify(dash, null, 2);
|
||||
const blob = new Blob([dashboardJsonPretty], {
|
||||
type: 'application/json;charset=utf-8',
|
||||
});
|
||||
const time = new Date().getTime();
|
||||
const title =
|
||||
typeof dash === 'object' && dash !== null && 'title' in dash && typeof dash.title === 'string'
|
||||
? dash.title
|
||||
: 'dashboard';
|
||||
saveAs(blob, `${title}-${time}.json`);
|
||||
saveAs(blob, `${dash.title}-${time}.json`);
|
||||
};
|
||||
|
||||
const openJsonModal = (clone: object) => {
|
||||
@@ -93,18 +80,13 @@ export const ShareExport = memo(({ dashboard, panel, onDismiss }: Props) => {
|
||||
<p>
|
||||
<Trans i18nKey="share-modal.export.info-text">Export this dashboard.</Trans>
|
||||
</p>
|
||||
<Field label={exportExternallyTranslation} noMargin>
|
||||
<Field label={exportExternallyTranslation}>
|
||||
<Switch id="share-externally-toggle" value={shareExternally} onChange={onShareExternallyChange} />
|
||||
</Field>
|
||||
<Modal.ButtonRow>
|
||||
<Button variant="secondary" onClick={onDismiss} fill="outline">
|
||||
<Trans i18nKey="share-modal.export.cancel-button">Cancel</Trans>
|
||||
</Button>
|
||||
{provisioningEnabled && isUnmanaged && (
|
||||
<Button variant="secondary" onClick={() => setShowExportToRepositoryDrawer(true)}>
|
||||
<Trans i18nKey="share-modal.export.export-to-repository-button">Export to Repository</Trans>
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="secondary" onClick={onViewJson}>
|
||||
<Trans i18nKey="share-modal.export.view-button">View JSON</Trans>
|
||||
</Button>
|
||||
@@ -112,30 +94,6 @@ export const ShareExport = memo(({ dashboard, panel, onDismiss }: Props) => {
|
||||
<Trans i18nKey="share-modal.export.save-button">Save to file</Trans>
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
{showExportToRepositoryDrawer && (
|
||||
<Drawer
|
||||
title={
|
||||
<Text variant="h3" element="h2">
|
||||
{t('share-modal.export.export-to-repository-title', 'Export Dashboard to Repository')}
|
||||
</Text>
|
||||
}
|
||||
subtitle={dashboard.title}
|
||||
onClose={() => setShowExportToRepositoryDrawer(false)}
|
||||
size="md"
|
||||
>
|
||||
<BulkExportProvisionedResource
|
||||
folderUid={dashboard.meta.folderUid}
|
||||
selectedItems={{
|
||||
dashboard: dashboard.uid ? { [dashboard.uid]: true } : {},
|
||||
folder: {},
|
||||
}}
|
||||
onDismiss={() => {
|
||||
setShowExportToRepositoryDrawer(false);
|
||||
onDismiss?.();
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user