Compare commits
10 Commits
dependabot
...
sriram/SQL
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40fd558587 | ||
|
|
c5bff2df50 | ||
|
|
c621dbc325 | ||
|
|
ecd3f0b490 | ||
|
|
2efcc88e62 | ||
|
|
6fea614106 | ||
|
|
c0c05a65fd | ||
|
|
41ed2aeb23 | ||
|
|
9e9233051e | ||
|
|
a5faedbe68 |
@@ -13,7 +13,7 @@ import (
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", NewReceiver(), &ReceiverList{}, resource.WithKind("Receiver"),
|
||||
resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{
|
||||
resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{resource.SelectableField{
|
||||
FieldSelector: "spec.title",
|
||||
FieldValueFunc: func(o resource.Object) (string, error) {
|
||||
cast, ok := o.(*Receiver)
|
||||
|
||||
@@ -790,6 +790,8 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -794,6 +794,8 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -301,6 +301,8 @@ var _ resource.ListObject = &DashboardList{}
|
||||
|
||||
// Copy methods for all subresource types
|
||||
|
||||
|
||||
|
||||
// DeepCopy creates a full deep copy of DashboardStatus
|
||||
func (s *DashboardStatus) DeepCopy() *DashboardStatus {
|
||||
cpy := &DashboardStatus{}
|
||||
|
||||
@@ -301,6 +301,8 @@ var _ resource.ListObject = &DashboardList{}
|
||||
|
||||
// Copy methods for all subresource types
|
||||
|
||||
|
||||
|
||||
// DeepCopy creates a full deep copy of DashboardStatus
|
||||
func (s *DashboardStatus) DeepCopy() *DashboardStatus {
|
||||
cpy := &DashboardStatus{}
|
||||
|
||||
@@ -794,6 +794,8 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -1411,6 +1411,8 @@ type DashboardVariableOption struct {
|
||||
Text DashboardStringOrArrayOfString `json:"text"`
|
||||
// Value of the option
|
||||
Value DashboardStringOrArrayOfString `json:"value"`
|
||||
// Additional properties for multi-props variables
|
||||
Properties map[string]string `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// NewDashboardVariableOption creates a new DashboardVariableOption object.
|
||||
|
||||
@@ -798,6 +798,8 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -1414,6 +1414,8 @@ type DashboardVariableOption struct {
|
||||
Text DashboardStringOrArrayOfString `json:"text"`
|
||||
// Value of the option
|
||||
Value DashboardStringOrArrayOfString `json:"value"`
|
||||
// Additional properties for multi-props variables
|
||||
Properties map[string]string `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// NewDashboardVariableOption creates a new DashboardVariableOption object.
|
||||
|
||||
4
apps/dashboard/pkg/apis/dashboard_manifest.go
generated
4
apps/dashboard/pkg/apis/dashboard_manifest.go
generated
File diff suppressed because one or more lines are too long
@@ -18,6 +18,8 @@ import (
|
||||
v1beta1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
||||
)
|
||||
|
||||
var ()
|
||||
|
||||
var appManifestData = app.ManifestData{
|
||||
AppName: "folder",
|
||||
Group: "folder.grafana.app",
|
||||
|
||||
@@ -41,9 +41,13 @@ Select a group to expand it and view the list of alert rules within that group.
|
||||
|
||||
The list view includes a number of filters to simplify managing large volumes of alerts.
|
||||
|
||||
## Filter and save searches
|
||||
|
||||
Click the **Filter** button to open the filter popup. You can filter by name, label, folder/namespace, evaluation group, data source, contact point, rule source, rule state, rule type, and the health of the alert rule from the popup menu. Click **Apply** at the bottom of the filter popup to enact the filters as you search.
|
||||
|
||||
{{< figure src="/media/docs/alerting/alerting-list-view-filter.png" max-width="750px" alt="Alert rule filter options" >}}
|
||||
Click the **Saved searches** button to open the list of previously saved searches, or click **+ Save current search** to add your current search to the saved searches list. You can also rename a saved search or set it as a default search. When you set a saved search as the default search, the Alert rules page opens with the search applied.
|
||||
|
||||
{{< figure src="/media/docs/alerting/alerting-saved-searches.png" max-width="750px" alt="Alert rule filter options" >}}
|
||||
|
||||
## Change alert rules list view
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ killercoda:
|
||||
|
||||
This tutorial is a continuation of the [Get started with Grafana Alerting - Route alerts using dynamic labels](http://www.grafana.com/tutorials/alerting-get-started-pt5/) tutorial.
|
||||
|
||||
{{< youtube id="mqj_hN24zLU" >}}
|
||||
|
||||
<!-- USE CASE -->
|
||||
|
||||
In this tutorial you will learn how to:
|
||||
|
||||
24
go.mod
24
go.mod
@@ -5,8 +5,8 @@ go 1.25.5
|
||||
require (
|
||||
buf.build/gen/go/parca-dev/parca/connectrpc/go v1.18.1-20250703125925-3f0fcf4bff96.1 // @grafana/observability-traces-and-profiling
|
||||
buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.36.2-20250703125925-3f0fcf4bff96.1 // @grafana/observability-traces-and-profiling
|
||||
cloud.google.com/go/kms v1.23.0 // @grafana/grafana-backend-group
|
||||
cloud.google.com/go/storage v1.58.0 // @grafana/grafana-backend-group
|
||||
cloud.google.com/go/kms v1.22.0 // @grafana/grafana-backend-group
|
||||
cloud.google.com/go/storage v1.55.0 // @grafana/grafana-backend-group
|
||||
connectrpc.com/connect v1.19.1 // @grafana/observability-traces-and-profiling
|
||||
cuelang.org/go v0.11.1 // @grafana/grafana-as-code
|
||||
dario.cat/mergo v1.0.2 // @grafana/grafana-app-platform-squad
|
||||
@@ -214,7 +214,7 @@ require (
|
||||
golang.org/x/time v0.14.0 // @grafana/grafana-backend-group
|
||||
golang.org/x/tools v0.40.0 // indirect; @grafana/grafana-as-code
|
||||
gonum.org/v1/gonum v0.16.0 // @grafana/oss-big-tent
|
||||
google.golang.org/api v0.256.0 // @grafana/grafana-backend-group
|
||||
google.golang.org/api v0.242.0 // @grafana/grafana-backend-group
|
||||
google.golang.org/grpc v1.77.0 // @grafana/plugins-platform-backend
|
||||
google.golang.org/protobuf v1.36.11 // @grafana/plugins-platform-backend
|
||||
gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-backend
|
||||
@@ -300,12 +300,12 @@ replace (
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go v0.121.4 // indirect
|
||||
cloud.google.com/go/auth v0.16.3 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.3 // indirect
|
||||
cloud.google.com/go/longrunning v0.7.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.2 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||
cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819 // indirect
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
|
||||
@@ -322,8 +322,8 @@ require (
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect
|
||||
github.com/FZambia/eagle v0.2.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
@@ -452,7 +452,7 @@ require (
|
||||
github.com/google/gnostic-models v0.7.1 // indirect
|
||||
github.com/google/go-github/v64 v64.0.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
@@ -621,7 +621,7 @@ require (
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.59.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
@@ -636,7 +636,7 @@ require (
|
||||
golang.org/x/term v0.38.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
|
||||
52
go.sum
52
go.sum
@@ -46,8 +46,8 @@ cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFO
|
||||
cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
|
||||
cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
|
||||
cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs=
|
||||
cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s=
|
||||
cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4=
|
||||
cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw=
|
||||
cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E=
|
||||
@@ -109,8 +109,8 @@ cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVo
|
||||
cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo=
|
||||
cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0=
|
||||
cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E=
|
||||
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
||||
cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc=
|
||||
cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=
|
||||
@@ -330,8 +330,8 @@ cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGE
|
||||
cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY=
|
||||
cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY=
|
||||
cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0=
|
||||
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
|
||||
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
|
||||
cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc=
|
||||
cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A=
|
||||
cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk=
|
||||
@@ -351,8 +351,8 @@ cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4
|
||||
cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w=
|
||||
cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24=
|
||||
cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI=
|
||||
cloud.google.com/go/kms v1.23.0 h1:WaqAZsUptyHwOo9II8rFC1Kd2I+yvNsNP2IJ14H2sUw=
|
||||
cloud.google.com/go/kms v1.23.0/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g=
|
||||
cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk=
|
||||
cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8=
|
||||
cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic=
|
||||
cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI=
|
||||
cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE=
|
||||
@@ -368,8 +368,8 @@ cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhX
|
||||
cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE=
|
||||
cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc=
|
||||
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
|
||||
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
|
||||
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
|
||||
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
|
||||
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
|
||||
cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE=
|
||||
cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM=
|
||||
cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA=
|
||||
@@ -558,8 +558,8 @@ cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeL
|
||||
cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
|
||||
cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
|
||||
cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4=
|
||||
cloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo=
|
||||
cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=
|
||||
cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0=
|
||||
cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY=
|
||||
cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w=
|
||||
cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I=
|
||||
cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4=
|
||||
@@ -727,12 +727,12 @@ github.com/FZambia/sentinel v1.0.0 h1:KJ0ryjKTZk5WMp0dXvSdNqp3lFaW1fNFuEYfrkLOYI
|
||||
github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
|
||||
github.com/IBM/pgxpoolprometheus v1.1.2 h1:sHJwxoL5Lw4R79Zt+H4Uj1zZ4iqXJLdk7XDE7TPs97U=
|
||||
@@ -1587,8 +1587,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
||||
@@ -2699,8 +2699,8 @@ go.opentelemetry.io/otel/exporters/prometheus v0.59.0 h1:HHf+wKS6o5++XZhS98wvILr
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.59.0/go.mod h1:R8GpRXTZrqvXHDEGVH5bF6+JqAZcK8PjJcZ5nGhEWiE=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 h1:12vMqzLLNZtXuXbJhSENRg+Vvx+ynNilV8twBLBsXMY=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2/go.mod h1:ZccPZoPOoq8x3Trik/fCsba7DEYDUnN6yX79pgp2BUQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 h1:6VjV6Et+1Hd2iLZEPtdV7vie80Yyqf7oikJLjQ/myi0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0/go.mod h1:u8hcp8ji5gaM/RfcOo8z9NMnf1pVLfVY7lBY2VOGuUU=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE=
|
||||
go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc=
|
||||
@@ -3377,8 +3377,8 @@ google.golang.org/api v0.118.0/go.mod h1:76TtD3vkgmZ66zZzp72bUUklpmQmKlhh6sYtIjY
|
||||
google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
|
||||
google.golang.org/api v0.124.0/go.mod h1:xu2HQurE5gi/3t1aFCvhPD781p0a3p11sdunTJ2BlP4=
|
||||
google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw=
|
||||
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
|
||||
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
|
||||
google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg=
|
||||
google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -3529,8 +3529,8 @@ google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOl
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/genproto v0.0.0-20230525234025-438c736192d0/go.mod h1:9ExIQyXL5hZrHzQceCwuSYwZZ5QZBazOcprJ5rgs3lY=
|
||||
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64=
|
||||
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc=
|
||||
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc=
|
||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs=
|
||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
|
||||
|
||||
@@ -400,10 +400,6 @@ export interface FeatureToggles {
|
||||
*/
|
||||
tableSharedCrosshair?: boolean;
|
||||
/**
|
||||
* Use the kubernetes API for feature toggle management in the frontend
|
||||
*/
|
||||
kubernetesFeatureToggles?: boolean;
|
||||
/**
|
||||
* Enabled grafana cloud specific RBAC roles
|
||||
*/
|
||||
cloudRBACRoles?: boolean;
|
||||
|
||||
174
packages/grafana-sql/src/SQLVariableSupport.tsx
Normal file
174
packages/grafana-sql/src/SQLVariableSupport.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
CustomVariableSupport,
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
QueryEditorProps,
|
||||
Field,
|
||||
DataFrame,
|
||||
MetricFindValue,
|
||||
} from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { EditorMode, EditorRows, EditorRow, EditorField } from '@grafana/plugin-ui';
|
||||
import { Combobox, ComboboxOption } from '@grafana/ui';
|
||||
|
||||
import { SqlQueryEditorLazy } from './components/QueryEditorLazy';
|
||||
import { SqlDatasource } from './datasource/SqlDatasource';
|
||||
import { applyQueryDefaults } from './defaults';
|
||||
import { QueryFormat, type SQLQuery, type SQLOptions, type SQLQueryMeta } from './types';
|
||||
|
||||
type SQLVariableQuery = { query: string } & SQLQuery;
|
||||
|
||||
const refId = 'SQLVariableQueryEditor-VariableQuery';
|
||||
|
||||
export class SQLVariableSupport extends CustomVariableSupport<SqlDatasource, SQLQuery> {
|
||||
constructor(readonly datasource: SqlDatasource) {
|
||||
super();
|
||||
}
|
||||
editor = SQLVariablesQueryEditor;
|
||||
query(request: DataQueryRequest<SQLQuery>): Observable<DataQueryResponse> {
|
||||
if (request.targets.length < 1) {
|
||||
throw new Error('no variable query found');
|
||||
}
|
||||
const updatedQuery = migrateVariableQuery(request.targets[0]);
|
||||
return this.datasource.query({ ...request, targets: [updatedQuery] }).pipe(
|
||||
map((d: DataQueryResponse) => {
|
||||
const frames = d.data || [];
|
||||
const metricFindValues = convertDataFramesToMetricFindValues(frames, updatedQuery.meta);
|
||||
return { data: metricFindValues };
|
||||
})
|
||||
);
|
||||
}
|
||||
getDefaultQuery(): Partial<SQLQuery> {
|
||||
return applyQueryDefaults({ refId, editorMode: EditorMode.Builder, format: QueryFormat.Table });
|
||||
}
|
||||
}
|
||||
|
||||
type SQLVariableQueryEditorProps = QueryEditorProps<SqlDatasource, SQLQuery, SQLOptions>;
|
||||
|
||||
const SQLVariablesQueryEditor = (props: SQLVariableQueryEditorProps) => {
|
||||
const query = migrateVariableQuery(props.query);
|
||||
return (
|
||||
<>
|
||||
<SqlQueryEditorLazy {...props} query={query} />
|
||||
<FieldMapping {...props} query={query} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FieldMapping = (props: SQLVariableQueryEditorProps) => {
|
||||
const { query, datasource, onChange } = props;
|
||||
const [choices, setChoices] = useState<ComboboxOption[]>([]);
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
// eslint-disable-next-line
|
||||
const subscription = datasource.query({ targets: [query] } as DataQueryRequest<SQLQuery>).subscribe({
|
||||
next: (response) => {
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
const fieldNames = (response.data[0] || { fields: [] }).fields.map((f: Field) => f.name);
|
||||
setChoices(fieldNames.map((f: Field) => ({ value: f, label: f })));
|
||||
},
|
||||
error: () => {
|
||||
if (isActive) {
|
||||
setChoices([]);
|
||||
}
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
isActive = false;
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [datasource, query]);
|
||||
const onMetaPropChange = <Key extends keyof SQLQueryMeta, Value extends SQLQueryMeta[Key]>(
|
||||
key: Key,
|
||||
value: Value,
|
||||
meta = query.meta || {}
|
||||
) => {
|
||||
onChange({ ...query, meta: { ...meta, [key]: value } });
|
||||
};
|
||||
return (
|
||||
<EditorRows>
|
||||
<EditorRow>
|
||||
<EditorField label={t('grafana-sql.components.query-meta.variables.valueField', 'Value Field')}>
|
||||
<Combobox
|
||||
isClearable
|
||||
value={query.meta?.valueField}
|
||||
onChange={(e) => onMetaPropChange('valueField', e?.value)}
|
||||
width={40}
|
||||
options={choices}
|
||||
/>
|
||||
</EditorField>
|
||||
<EditorField label={t('grafana-sql.components.query-meta.variables.textField', 'Text Field')}>
|
||||
<Combobox
|
||||
isClearable
|
||||
value={query.meta?.textField}
|
||||
onChange={(e) => onMetaPropChange('textField', e?.value)}
|
||||
width={40}
|
||||
options={choices}
|
||||
/>
|
||||
</EditorField>
|
||||
</EditorRow>
|
||||
</EditorRows>
|
||||
);
|
||||
};
|
||||
|
||||
const migrateVariableQuery = (rawQuery: string | SQLQuery): SQLVariableQuery => {
|
||||
if (typeof rawQuery !== 'string') {
|
||||
return {
|
||||
...rawQuery,
|
||||
refId: rawQuery.refId || refId,
|
||||
query: rawQuery.rawSql || '',
|
||||
};
|
||||
}
|
||||
return {
|
||||
...applyQueryDefaults({
|
||||
refId,
|
||||
rawSql: rawQuery,
|
||||
editorMode: rawQuery ? EditorMode.Code : EditorMode.Builder,
|
||||
}),
|
||||
query: rawQuery,
|
||||
};
|
||||
};
|
||||
|
||||
const convertDataFramesToMetricFindValues = (frames: DataFrame[], meta?: SQLQueryMeta): MetricFindValue[] => {
|
||||
if (!frames.length) {
|
||||
throw new Error('no results found');
|
||||
}
|
||||
|
||||
const frame = frames[0];
|
||||
|
||||
const fields = frame.fields;
|
||||
|
||||
if (fields.length < 1) {
|
||||
throw new Error('no fields found in the response');
|
||||
}
|
||||
|
||||
let textField = fields.find((f) => f.name === '__text');
|
||||
let valueField = fields.find((f) => f.name === '__value');
|
||||
if (meta?.textField) {
|
||||
textField = fields.find((f) => f.name === meta.textField);
|
||||
}
|
||||
if (meta?.valueField) {
|
||||
valueField = fields.find((f) => f.name === meta.valueField);
|
||||
}
|
||||
const resolvedTextField = textField || valueField || fields[0];
|
||||
const resolvedValueField = valueField || textField || fields[0];
|
||||
|
||||
const results: MetricFindValue[] = [];
|
||||
const rowCount = frame.length;
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const text = String(resolvedTextField.values[i] ?? '');
|
||||
const value = String(resolvedValueField.values[i] ?? '');
|
||||
const properties: Record<string, string> = {};
|
||||
for (const field of fields) {
|
||||
properties[field.name] = String(field.values[i] ?? '');
|
||||
}
|
||||
results.push({ text, value, properties });
|
||||
}
|
||||
return results;
|
||||
};
|
||||
@@ -21,6 +21,7 @@ export { TLSSecretsConfig } from './components/configuration/TLSSecretsConfig';
|
||||
export { useMigrateDatabaseFields } from './components/configuration/useMigrateDatabaseFields';
|
||||
export { SqlQueryEditorLazy } from './components/QueryEditorLazy';
|
||||
export type { QueryHeaderProps } from './components/QueryHeader';
|
||||
export { SQLVariableSupport } from './SQLVariableSupport';
|
||||
export { createSelectClause, haveColumns } from './utils/sql.utils';
|
||||
export { applyQueryDefaults } from './defaults';
|
||||
export { makeVariable } from './utils/testHelpers';
|
||||
|
||||
@@ -69,6 +69,12 @@
|
||||
"placeholder-select-format": "Select format",
|
||||
"run-query": "Run query"
|
||||
},
|
||||
"query-meta": {
|
||||
"variables": {
|
||||
"textField": "Text Field",
|
||||
"valueField": "Value Field"
|
||||
}
|
||||
},
|
||||
"query-toolbox": {
|
||||
"content-hit-ctrlcmdreturn-to-run-query": "Hit CTRL/CMD+Return to run query",
|
||||
"tooltip-collapse": "Collapse editor",
|
||||
|
||||
@@ -50,6 +50,8 @@ export enum QueryFormat {
|
||||
Table = 'table',
|
||||
}
|
||||
|
||||
export type SQLQueryMeta = { valueField?: string; textField?: string };
|
||||
|
||||
export interface SQLQuery extends DataQuery {
|
||||
alias?: string;
|
||||
format?: QueryFormat;
|
||||
@@ -59,6 +61,7 @@ export interface SQLQuery extends DataQuery {
|
||||
sql?: SQLExpression;
|
||||
editorMode?: EditorMode;
|
||||
rawQuery?: boolean;
|
||||
meta?: SQLQueryMeta;
|
||||
}
|
||||
|
||||
export interface NameValue {
|
||||
|
||||
@@ -650,13 +650,6 @@ var (
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaDatavizSquad,
|
||||
},
|
||||
{
|
||||
Name: "kubernetesFeatureToggles",
|
||||
Description: "Use the kubernetes API for feature toggle management in the frontend",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaOperatorExperienceSquad,
|
||||
},
|
||||
{
|
||||
Name: "cloudRBACRoles",
|
||||
Description: "Enabled grafana cloud specific RBAC roles",
|
||||
|
||||
1
pkg/services/featuremgmt/toggles_gen.csv
generated
1
pkg/services/featuremgmt/toggles_gen.csv
generated
@@ -90,7 +90,6 @@ pdfTables,preview,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true
|
||||
timeComparison,experimental,@grafana/dataviz-squad,false,false,true
|
||||
tableSharedCrosshair,experimental,@grafana/dataviz-squad,false,false,true
|
||||
kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad,false,false,true
|
||||
cloudRBACRoles,preview,@grafana/identity-access-team,false,true,false
|
||||
alertingQueryOptimization,GA,@grafana/alerting-squad,false,false,false
|
||||
jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,false,true,false
|
||||
|
||||
|
3
pkg/services/featuremgmt/toggles_gen.json
generated
3
pkg/services/featuremgmt/toggles_gen.json
generated
@@ -2044,7 +2044,8 @@
|
||||
"metadata": {
|
||||
"name": "kubernetesFeatureToggles",
|
||||
"resourceVersion": "1764664939750",
|
||||
"creationTimestamp": "2024-01-18T05:32:44Z"
|
||||
"creationTimestamp": "2024-01-18T05:32:44Z",
|
||||
"deletionTimestamp": "2026-01-07T12:02:51Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Use the kubernetes API for feature toggle management in the frontend",
|
||||
|
||||
@@ -3,7 +3,8 @@ import { render, screen, userEvent, waitFor } from 'test/test-utils';
|
||||
import { byLabelText, byRole, byText } from 'testing-library-selector';
|
||||
|
||||
import { setPluginLinksHook } from '@grafana/runtime';
|
||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import server from '@grafana/test-utils/server';
|
||||
import { mockAlertRuleApi, setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting';
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
mockPluginLinkExtension,
|
||||
mockPromAlertingRule,
|
||||
mockRulerGrafanaRecordingRule,
|
||||
mockRulerGrafanaRule,
|
||||
} from '../../mocks';
|
||||
import { grafanaRulerRule } from '../../mocks/grafanaRulerApi';
|
||||
import { grantPermissionsHelper } from '../../test/test-utils';
|
||||
@@ -130,6 +132,8 @@ const dataSources = {
|
||||
};
|
||||
|
||||
describe('RuleViewer', () => {
|
||||
const api = mockAlertRuleApi(server);
|
||||
|
||||
beforeEach(() => {
|
||||
setupDataSources(...Object.values(dataSources));
|
||||
});
|
||||
@@ -249,19 +253,22 @@ describe('RuleViewer', () => {
|
||||
|
||||
expect(screen.getAllByRole('row')).toHaveLength(7);
|
||||
expect(screen.getAllByRole('row')[1]).toHaveTextContent(/6Provisioning2025-01-18 04:35:17/i);
|
||||
expect(screen.getAllByRole('row')[1]).toHaveTextContent('+3-3Latest');
|
||||
expect(screen.getAllByRole('row')[1]).toHaveTextContent('Updated by provisioning service');
|
||||
expect(screen.getAllByRole('row')[1]).toHaveTextContent('+4-3Latest');
|
||||
|
||||
expect(screen.getAllByRole('row')[2]).toHaveTextContent(/5Alerting2025-01-17 04:35:17/i);
|
||||
expect(screen.getAllByRole('row')[2]).toHaveTextContent('+5-5');
|
||||
expect(screen.getAllByRole('row')[2]).toHaveTextContent('+5-6');
|
||||
|
||||
expect(screen.getAllByRole('row')[3]).toHaveTextContent(/4different user2025-01-16 04:35:17/i);
|
||||
expect(screen.getAllByRole('row')[3]).toHaveTextContent('+5-5');
|
||||
expect(screen.getAllByRole('row')[3]).toHaveTextContent('Changed alert title and thresholds');
|
||||
expect(screen.getAllByRole('row')[3]).toHaveTextContent('+6-5');
|
||||
|
||||
expect(screen.getAllByRole('row')[4]).toHaveTextContent(/3user12025-01-15 04:35:17/i);
|
||||
expect(screen.getAllByRole('row')[4]).toHaveTextContent('+5-9');
|
||||
expect(screen.getAllByRole('row')[4]).toHaveTextContent('+5-10');
|
||||
|
||||
expect(screen.getAllByRole('row')[5]).toHaveTextContent(/2User ID foo2025-01-14 04:35:17/i);
|
||||
expect(screen.getAllByRole('row')[5]).toHaveTextContent('+11-7');
|
||||
expect(screen.getAllByRole('row')[5]).toHaveTextContent('Updated evaluation interval and routing');
|
||||
expect(screen.getAllByRole('row')[5]).toHaveTextContent('+12-7');
|
||||
|
||||
expect(screen.getAllByRole('row')[6]).toHaveTextContent(/1Unknown 2025-01-13 04:35:17/i);
|
||||
|
||||
@@ -275,9 +282,10 @@ describe('RuleViewer', () => {
|
||||
await renderRuleViewer(mockRule, mockRuleIdentifier, ActiveTab.VersionHistory);
|
||||
expect(await screen.findByRole('button', { name: /Compare versions/i })).toBeDisabled();
|
||||
|
||||
expect(screen.getByRole('cell', { name: /provisioning/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('cell', { name: /alerting/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('cell', { name: /Unknown/i })).toBeInTheDocument();
|
||||
// Check for special updated_by values - use getAllByRole since some text appears in multiple columns
|
||||
expect(screen.getAllByRole('cell', { name: /provisioning/i }).length).toBeGreaterThan(0);
|
||||
expect(screen.getByRole('cell', { name: /^alerting$/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('cell', { name: /^Unknown$/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('cell', { name: /user id foo/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -321,6 +329,47 @@ describe('RuleViewer', () => {
|
||||
await renderRuleViewer(rule, ruleIdentifier);
|
||||
expect(screen.queryByText('Labels')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Notes column when versions have messages', async () => {
|
||||
await renderRuleViewer(mockRule, mockRuleIdentifier, ActiveTab.VersionHistory);
|
||||
|
||||
expect(await screen.findByRole('columnheader', { name: /Notes/i })).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('row')).toHaveLength(7); // 1 header + 6 data rows
|
||||
expect(screen.getByRole('cell', { name: /Updated by provisioning service/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('cell', { name: /Changed alert title and thresholds/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('cell', { name: /Updated evaluation interval and routing/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show Notes column when no versions have messages', async () => {
|
||||
const versionsWithoutMessages = [
|
||||
mockRulerGrafanaRule(
|
||||
{},
|
||||
{
|
||||
uid: grafanaRulerRule.grafana_alert.uid,
|
||||
version: 2,
|
||||
updated: '2025-01-14T09:35:17.000Z',
|
||||
updated_by: { uid: 'foo', name: '' },
|
||||
}
|
||||
),
|
||||
mockRulerGrafanaRule(
|
||||
{},
|
||||
{
|
||||
uid: grafanaRulerRule.grafana_alert.uid,
|
||||
version: 1,
|
||||
updated: '2025-01-13T09:35:17.000Z',
|
||||
updated_by: null,
|
||||
}
|
||||
),
|
||||
];
|
||||
api.getAlertRuleVersionHistory(grafanaRulerRule.grafana_alert.uid, versionsWithoutMessages);
|
||||
|
||||
await renderRuleViewer(mockRule, mockRuleIdentifier, ActiveTab.VersionHistory);
|
||||
|
||||
await screen.findByRole('button', { name: /Compare versions/i });
|
||||
|
||||
expect(screen.getAllByRole('row')).toHaveLength(3); // 1 header + 2 data rows
|
||||
expect(screen.queryByRole('columnheader', { name: /Notes/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Badge, Button, Checkbox, Column, InteractiveTable, Stack, Text } from '@grafana/ui';
|
||||
import { Badge, Button, Checkbox, Column, InteractiveTable, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { computeVersionDiff } from 'app/features/alerting/unified/utils/diff';
|
||||
import { RuleIdentifier } from 'app/types/unified-alerting';
|
||||
@@ -33,6 +34,7 @@ export function VersionHistoryTable({
|
||||
onRestoreError,
|
||||
canRestore,
|
||||
}: VersionHistoryTableProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
const [ruleToRestore, setRuleToRestore] = useState<RulerGrafanaRuleDTO<GrafanaRuleDefinition>>();
|
||||
const ruleToRestoreUid = ruleToRestore?.grafana_alert?.uid ?? '';
|
||||
@@ -41,6 +43,8 @@ export function VersionHistoryTable({
|
||||
[ruleToRestoreUid]
|
||||
);
|
||||
|
||||
const hasAnyNotes = useMemo(() => ruleVersions.some((v) => v.grafana_alert.message), [ruleVersions]);
|
||||
|
||||
const showConfirmation = (ruleToRestore: RulerGrafanaRuleDTO<GrafanaRuleDefinition>) => {
|
||||
setShowConfirmModal(true);
|
||||
setRuleToRestore(ruleToRestore);
|
||||
@@ -52,6 +56,15 @@ export function VersionHistoryTable({
|
||||
|
||||
const unknown = t('alerting.alertVersionHistory.unknown', 'Unknown');
|
||||
|
||||
const notesColumn: Column<RulerGrafanaRuleDTO<GrafanaRuleDefinition>> = {
|
||||
id: 'notes',
|
||||
header: t('core.versionHistory.table.notes', 'Notes'),
|
||||
cell: ({ row }) => {
|
||||
const message = row.original.grafana_alert.message;
|
||||
return message || null;
|
||||
},
|
||||
};
|
||||
|
||||
const columns: Array<Column<RulerGrafanaRuleDTO<GrafanaRuleDefinition>>> = [
|
||||
{
|
||||
disableGrow: true,
|
||||
@@ -91,9 +104,12 @@ export function VersionHistoryTable({
|
||||
if (!value) {
|
||||
return unknown;
|
||||
}
|
||||
return dateTimeFormat(value) + ' (' + dateTimeFormatTimeAgo(value) + ')';
|
||||
return (
|
||||
<span className={styles.nowrap}>{dateTimeFormat(value) + ' (' + dateTimeFormatTimeAgo(value) + ')'}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
...(hasAnyNotes ? [notesColumn] : []),
|
||||
{
|
||||
id: 'diff',
|
||||
disableGrow: true,
|
||||
@@ -179,3 +195,9 @@ export function VersionHistoryTable({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = () => ({
|
||||
nowrap: css({
|
||||
whiteSpace: 'nowrap',
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -154,6 +154,7 @@ export const rulerRuleVersionHistoryHandler = () => {
|
||||
uid: 'service',
|
||||
name: '',
|
||||
};
|
||||
draft.grafana_alert.message = 'Updated by provisioning service';
|
||||
}),
|
||||
produce(grafanaRulerRule, (draft: RulerGrafanaRuleDTO<GrafanaRuleDefinition>) => {
|
||||
draft.grafana_alert.version = 5;
|
||||
@@ -171,6 +172,7 @@ export const rulerRuleVersionHistoryHandler = () => {
|
||||
uid: 'different',
|
||||
name: 'different user',
|
||||
};
|
||||
draft.grafana_alert.message = 'Changed alert title and thresholds';
|
||||
}),
|
||||
produce(grafanaRulerRule, (draft: RulerGrafanaRuleDTO<GrafanaRuleDefinition>) => {
|
||||
draft.grafana_alert.version = 3;
|
||||
@@ -193,6 +195,7 @@ export const rulerRuleVersionHistoryHandler = () => {
|
||||
uid: 'foo',
|
||||
name: '',
|
||||
};
|
||||
draft.grafana_alert.message = 'Updated evaluation interval and routing';
|
||||
}),
|
||||
produce(grafanaRulerRule, (draft: RulerGrafanaRuleDTO<GrafanaRuleDefinition>) => {
|
||||
draft.grafana_alert.version = 1;
|
||||
|
||||
@@ -284,6 +284,7 @@ function variableValueOptionsToVariableOptions(varState: MultiValueVariable['sta
|
||||
value: String(o.value),
|
||||
text: o.label,
|
||||
selected: Array.isArray(varState.value) ? varState.value.includes(o.value) : varState.value === o.value,
|
||||
...(o.properties && { properties: o.properties }),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR } from '@grafana/ui/internal';
|
||||
import { LogsFrame } from 'app/features/logs/logsFrame';
|
||||
import { DATAPLANE_ID_NAME, LogsFrame } from 'app/features/logs/logsFrame';
|
||||
|
||||
import { getFieldLinksForExplore } from '../utils/links';
|
||||
|
||||
@@ -154,9 +154,9 @@ export function LogsTable(props: Props) {
|
||||
},
|
||||
});
|
||||
// `getLinks` and `applyFieldOverrides` are taken from TableContainer.tsx
|
||||
for (const [index, field] of frameWithOverrides.fields.entries()) {
|
||||
for (const [fieldIdx, field] of frameWithOverrides.fields.entries()) {
|
||||
// Hide ID field from visualization (it's only needed for row matching)
|
||||
if (logsFrame?.idField && (field.name === logsFrame.idField.name || field.name === 'id')) {
|
||||
if (logsFrame?.idField && (field.name === logsFrame.idField.name || field.name === DATAPLANE_ID_NAME)) {
|
||||
field.config = {
|
||||
...field.config,
|
||||
custom: {
|
||||
@@ -180,7 +180,7 @@ export function LogsTable(props: Props) {
|
||||
};
|
||||
|
||||
// For the first field (time), wrap the cell to include action buttons
|
||||
const isFirstField = index === 0;
|
||||
const isFirstField = fieldIdx === 0;
|
||||
|
||||
field.config = {
|
||||
...field.config,
|
||||
@@ -202,7 +202,6 @@ export function LogsTable(props: Props) {
|
||||
panelState={props.panelState}
|
||||
absoluteRange={props.absoluteRange}
|
||||
logRows={props.logRows}
|
||||
rowIndex={cellProps.rowIndex}
|
||||
/>
|
||||
<span className={styles.firstColumnCell}>
|
||||
{cellProps.field.display?.(cellProps.value).text ?? String(cellProps.value)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useState, memo } from 'react';
|
||||
|
||||
import {
|
||||
AbsoluteTimeRange,
|
||||
@@ -13,7 +13,7 @@ import { t } from '@grafana/i18n';
|
||||
import { ClipboardButton, CustomCellRendererProps, IconButton, Modal, useTheme2 } from '@grafana/ui';
|
||||
import { getLogsPermalinkRange } from 'app/core/utils/shortLinks';
|
||||
import { getUrlStateFromPaneState } from 'app/features/explore/hooks/useStateSync';
|
||||
import { LogsFrame } from 'app/features/logs/logsFrame';
|
||||
import { LogsFrame, DATAPLANE_ID_NAME } from 'app/features/logs/logsFrame';
|
||||
import { getState } from 'app/store/store';
|
||||
|
||||
import { getExploreBaseUrl } from './utils/url';
|
||||
@@ -28,25 +28,20 @@ interface Props extends CustomCellRendererProps {
|
||||
index?: number;
|
||||
}
|
||||
|
||||
export function LogsTableActionButtons(props: Props) {
|
||||
export const LogsTableActionButtons = memo((props: Props) => {
|
||||
const { exploreId, absoluteRange, logRows, rowIndex, panelState, displayedFields, logsFrame, frame } = props;
|
||||
|
||||
const theme = useTheme2();
|
||||
const [isInspecting, setIsInspecting] = useState(false);
|
||||
// Get logId from the table frame (frame), not the original logsFrame, because
|
||||
// the table frame is sorted/transformed and rowIndex refers to the table frame
|
||||
const idFieldName = logsFrame?.idField?.name ?? 'id';
|
||||
const idField = frame.fields.find((field) => field.name === idFieldName || field.name === 'id');
|
||||
const idFieldName = logsFrame?.idField?.name ?? DATAPLANE_ID_NAME;
|
||||
const idField = frame.fields.find((field) => field.name === idFieldName || field.name === DATAPLANE_ID_NAME);
|
||||
const logId = idField?.values[rowIndex];
|
||||
const getLineValue = () => {
|
||||
const bodyFieldName = logsFrame?.bodyField?.name;
|
||||
const bodyField = bodyFieldName
|
||||
? frame.fields.find((field) => field.name === bodyFieldName)
|
||||
: frame.fields.find((field) => field.type === 'string');
|
||||
return bodyField?.values[rowIndex];
|
||||
};
|
||||
|
||||
const lineValue = getLineValue();
|
||||
const getLineValue = () => {
|
||||
const logRowById = logRows?.find((row) => row.rowId === logId);
|
||||
return logRowById?.raw ?? '';
|
||||
};
|
||||
|
||||
const styles = getStyles(theme);
|
||||
|
||||
@@ -105,33 +100,29 @@ export function LogsTableActionButtons(props: Props) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.iconWrapper}>
|
||||
<div className={styles.inspect}>
|
||||
<IconButton
|
||||
className={styles.inspectButton}
|
||||
tooltip={t('explore.logs-table.action-buttons.view-log-line', 'View log line')}
|
||||
variant="secondary"
|
||||
aria-label={t('explore.logs-table.action-buttons.view-log-line', 'View log line')}
|
||||
tooltipPlacement="top"
|
||||
size="md"
|
||||
name="eye"
|
||||
onClick={handleViewClick}
|
||||
tabIndex={0}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.inspect}>
|
||||
<ClipboardButton
|
||||
className={styles.clipboardButton}
|
||||
icon="share-alt"
|
||||
variant="secondary"
|
||||
fill="text"
|
||||
size="md"
|
||||
tooltip={t('explore.logs-table.action-buttons.copy-link', 'Copy link to log line')}
|
||||
tooltipPlacement="top"
|
||||
tabIndex={0}
|
||||
aria-label={t('explore.logs-table.action-buttons.copy-link', 'Copy link to log line')}
|
||||
getText={getText}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
className={styles.icon}
|
||||
tooltip={t('explore.logs-table.action-buttons.view-log-line', 'View log line')}
|
||||
variant="secondary"
|
||||
aria-label={t('explore.logs-table.action-buttons.view-log-line', 'View log line')}
|
||||
tooltipPlacement="top"
|
||||
size="md"
|
||||
name="eye"
|
||||
onClick={handleViewClick}
|
||||
tabIndex={0}
|
||||
/>
|
||||
<ClipboardButton
|
||||
className={styles.icon}
|
||||
icon="share-alt"
|
||||
variant="secondary"
|
||||
fill="text"
|
||||
size="md"
|
||||
tooltip={t('explore.logs-table.action-buttons.copy-link', 'Copy link to log line')}
|
||||
tooltipPlacement="top"
|
||||
tabIndex={0}
|
||||
aria-label={t('explore.logs-table.action-buttons.copy-link', 'Copy link to log line')}
|
||||
getText={getText}
|
||||
/>
|
||||
</div>
|
||||
{isInspecting && (
|
||||
<Modal
|
||||
@@ -139,9 +130,9 @@ export function LogsTableActionButtons(props: Props) {
|
||||
isOpen={true}
|
||||
title={t('explore.logs-table.action-buttons.inspect-value', 'Inspect value')}
|
||||
>
|
||||
<pre>{lineValue}</pre>
|
||||
<pre>{getLineValue()}</pre>
|
||||
<Modal.ButtonRow>
|
||||
<ClipboardButton icon="copy" getText={() => lineValue}>
|
||||
<ClipboardButton icon="copy" getText={() => getLineValue()}>
|
||||
{t('explore.logs-table.action-buttons.copy-to-clipboard', 'Copy to Clipboard')}
|
||||
</ClipboardButton>
|
||||
</Modal.ButtonRow>
|
||||
@@ -149,15 +140,11 @@ export function LogsTableActionButtons(props: Props) {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => ({
|
||||
clipboardButton: css({
|
||||
height: '100%',
|
||||
lineHeight: '1',
|
||||
padding: 0,
|
||||
width: '20px',
|
||||
}),
|
||||
LogsTableActionButtons.displayName = 'LogsTableActionButtons';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
iconWrapper: css({
|
||||
background: theme.colors.background.secondary,
|
||||
boxShadow: theme.shadows.z2,
|
||||
@@ -166,25 +153,50 @@ export const getStyles = (theme: GrafanaTheme2) => ({
|
||||
height: '35px',
|
||||
left: 0,
|
||||
top: 0,
|
||||
padding: `0 ${theme.spacing(0.5)}`,
|
||||
padding: 0,
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
alignItems: 'center',
|
||||
// Fix switching icon direction when cell is numeric (rtl)
|
||||
direction: 'ltr',
|
||||
}),
|
||||
inspect: css({
|
||||
'& button svg': {
|
||||
marginRight: 'auto',
|
||||
icon: css({
|
||||
gap: 0,
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
width: '28px',
|
||||
height: '32px',
|
||||
display: 'inline-flex',
|
||||
justifyContent: 'center',
|
||||
|
||||
'&:before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
width: 24,
|
||||
height: 24,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
margin: 'auto',
|
||||
borderRadius: theme.shape.radius.default,
|
||||
backgroundColor: theme.colors.background.primary,
|
||||
zIndex: -1,
|
||||
opacity: 0,
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transitionDuration: '0.2s',
|
||||
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transitionProperty: 'opacity',
|
||||
},
|
||||
},
|
||||
'&:hover': {
|
||||
color: theme.colors.text.link,
|
||||
cursor: 'pointer',
|
||||
background: 'none',
|
||||
'&:before': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
padding: '5px 3px',
|
||||
}),
|
||||
inspectButton: css({
|
||||
borderRadius: theme.shape.radius.default,
|
||||
display: 'inline-flex',
|
||||
margin: 0,
|
||||
overflow: 'hidden',
|
||||
verticalAlign: 'middle',
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ function getField(cache: FieldCache, name: string, fieldType: FieldType): FieldW
|
||||
const DATAPLANE_TIMESTAMP_NAME = 'timestamp';
|
||||
const DATAPLANE_BODY_NAME = 'body';
|
||||
const DATAPLANE_SEVERITY_NAME = 'severity';
|
||||
const DATAPLANE_ID_NAME = 'id';
|
||||
export const DATAPLANE_ID_NAME = 'id';
|
||||
const DATAPLANE_LABELS_NAME = 'labels';
|
||||
|
||||
// NOTE: this is a hot fn, we need to avoid allocating new objects here
|
||||
|
||||
@@ -80,18 +80,18 @@ const buildLabelPath = (label: string) => {
|
||||
return label.includes('.') || label.trim().includes(' ') ? `["${label}"]` : `.${label}`;
|
||||
};
|
||||
|
||||
const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
|
||||
if (!('valuesFormat' in variable) || variable.valuesFormat !== 'json') {
|
||||
return [];
|
||||
}
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> => {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
};
|
||||
|
||||
function collectFieldPaths(option: Record<string, string>, currentPath: string) {
|
||||
const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
|
||||
function collectFieldPaths(option: Record<string, unknown>, currentPath: string): string[] {
|
||||
let paths: string[] = [];
|
||||
for (const field in option) {
|
||||
if (option.hasOwnProperty(field)) {
|
||||
const newPath = `${currentPath}.${field}`;
|
||||
const value = option[field];
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (isRecord(value)) {
|
||||
paths = [...paths, ...collectFieldPaths(value, newPath)];
|
||||
}
|
||||
paths.push(newPath);
|
||||
@@ -100,11 +100,23 @@ const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
|
||||
return paths;
|
||||
}
|
||||
|
||||
try {
|
||||
return collectFieldPaths(JSON.parse(variable.query)[0], variable.name);
|
||||
} catch {
|
||||
return [];
|
||||
if ('valuesFormat' in variable && variable.valuesFormat === 'json') {
|
||||
try {
|
||||
return collectFieldPaths(JSON.parse(variable.query)[0], variable.name);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
if ('options' in variable && Array.isArray(variable.options) && variable.options.length > 0) {
|
||||
for (const opt of variable.options) {
|
||||
if ('properties' in opt && isRecord(opt.properties) && Object.keys(opt.properties).length > 0) {
|
||||
return collectFieldPaths(opt.properties, variable.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
SQLQuery,
|
||||
SQLSelectableValue,
|
||||
SqlDatasource,
|
||||
SQLVariableSupport,
|
||||
formatSQL,
|
||||
} from '@grafana/sql';
|
||||
|
||||
@@ -25,6 +26,7 @@ export class PostgresDatasource extends SqlDatasource {
|
||||
|
||||
constructor(instanceSettings: DataSourceInstanceSettings<PostgresOptions>) {
|
||||
super(instanceSettings);
|
||||
this.variables = new SQLVariableSupport(this);
|
||||
}
|
||||
|
||||
getQueryModel(target?: SQLQuery, templateSrv?: TemplateSrv, scopedVars?: ScopedVars): PostgresQueryModel {
|
||||
|
||||
@@ -762,6 +762,19 @@ describe('Tempo service graph view', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should escape span with multi line content correctly', () => {
|
||||
const spanContent = [
|
||||
`
|
||||
SELECT * from "my_table"
|
||||
WHERE "data_enabled" = 1
|
||||
ORDER BY "name" ASC`,
|
||||
];
|
||||
let escaped = getEscapedRegexValues(getEscapedValues(spanContent));
|
||||
expect(escaped).toEqual([
|
||||
'\\n SELECT \\\\* from \\"my_table\\"\\n WHERE \\"data_enabled\\" = 1\\n ORDER BY \\"name\\" ASC',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get field config correctly', () => {
|
||||
let datasourceUid = 's4Jvz8Qnk';
|
||||
let tempoDatasourceUid = 'EbPO1fYnz';
|
||||
|
||||
@@ -1168,7 +1168,7 @@ export function getEscapedRegexValues(values: string[]) {
|
||||
}
|
||||
|
||||
export function getEscapedValues(values: string[]) {
|
||||
return values.map((value: string) => value.replace(/["\\]/g, '\\$&'));
|
||||
return values.map((value: string) => value.replace(/["\\]/g, '\\$&').replace(/[\n]/g, '\\n'));
|
||||
}
|
||||
|
||||
export function getFieldConfig(
|
||||
|
||||
@@ -293,6 +293,7 @@ export interface GrafanaRuleDefinition extends PostableGrafanaRuleDefinition {
|
||||
updated?: string;
|
||||
updated_by?: UpdatedBy | null;
|
||||
version?: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// types for Grafana-managed recording and alerting rules
|
||||
|
||||
@@ -4416,6 +4416,7 @@
|
||||
},
|
||||
"no-properties-changed": "No relevant properties changed",
|
||||
"table": {
|
||||
"notes": "Notes",
|
||||
"updated": "Date",
|
||||
"updatedBy": "Updated By",
|
||||
"version": "Version"
|
||||
|
||||
Reference in New Issue
Block a user