merge main

This commit is contained in:
Ryan McKinley 2024-06-20 17:05:36 +03:00
commit a115bb6214
60 changed files with 1394 additions and 1507 deletions

View File

@ -1,9 +1,14 @@
name: Trivy Scan
on:
pull_request:
# only run on PRs where go.mod/go.sum/etc have been updated
paths:
- go.*
push:
branches:
- main
paths:
- go.*
jobs:
trivy-scan:
@ -25,6 +30,8 @@ jobs:
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'
trivyignores: .trivyignore
# for the PR check, ignore JS-related issues
skip-files: 'yarn.lock,package.json'
- name: Run Trivy vulnerability scanner (SARIF)
uses: aquasecurity/trivy-action@0.22.0
with:

View File

@ -72,6 +72,10 @@
"pr": {
"type": "number",
"description": "GitHub pull request the plugin was built from"
},
"build": {
"type": "number",
"description": "Build job number used to build this plugin."
}
}
},

View File

@ -138,7 +138,6 @@ Experimental features might be changed or removed without prior notice.
| `pluginsFrontendSandbox` | Enables the plugins frontend sandbox |
| `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) |
| `vizAndWidgetSplit` | Split panels between visualizations and widgets |
| `prometheusIncrementalQueryInstrumentation` | Adds RudderStack events to incremental queries |
| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers |
| `mlExpressions` | Enable support for Machine Learning in server-side expressions |
| `metricsSummary` | Enables metrics summary queries in the Tempo data source |
@ -182,7 +181,6 @@ Experimental features might be changed or removed without prior notice.
| `accessActionSets` | Introduces action sets for resource permissions |
| `disableNumericMetricsSortingInExpressions` | In server-side expressions, disable the sorting of numeric-kind metrics by their metric name or labels. |
| `queryLibrary` | Enables Query Library feature in Explore |
| `autofixDSUID` | Automatically migrates invalid datasource UIDs |
| `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore |
| `newDashboardSharingComponent` | Enables the new sharing drawer design |
| `alertingListViewV2` | Enables the new alert list view design |
@ -191,6 +189,7 @@ Experimental features might be changed or removed without prior notice.
| `alertingCentralAlertHistory` | Enables the new central alert history. |
| `azureMonitorPrometheusExemplars` | Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars |
| `pinNavItems` | Enables pinning of nav items |
| `failWrongDSUID` | Throws an error if a datasource has an invalid UIDs |
| `databaseReadReplica` | Use a read replica for some database queries. |
## Development feature toggles

View File

@ -379,7 +379,7 @@ _Generally available in all editions of Grafana_
Use the Grafana Alerting - Grafana OnCall integration to effortlessly connect alerts generated by Grafana Alerting with Grafana OnCall. From there, you can route them according to defined escalation chains and schedules.
To learn more, refer to the [Grafana OnCall integration for Alerting documentation](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/manage-contact-points/integrations/configure-oncall/), as well as the following video demo.
To learn more, refer to the [Grafana OnCall integration for Alerting documentation](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/alerting/configure-notifications/manage-contact-points/integrations/configure-oncall/), as well as the following video demo.
{{< youtube id="abRn5I61hxs?rel=0" >}}

39
go.mod
View File

@ -33,7 +33,7 @@ require (
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // @grafana/plugins-platform-backend
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // @grafana/grafana-backend-group
github.com/alicebob/miniredis/v2 v2.30.1 // @grafana/alerting-backend
github.com/andybalholm/brotli v1.0.5 // @grafana/partner-datasources
github.com/andybalholm/brotli v1.0.6 // @grafana/partner-datasources
github.com/apache/arrow/go/v15 v15.0.2 // @grafana/observability-metrics
github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad
github.com/aws/aws-sdk-go v1.51.31 // @grafana/aws-datasources
@ -122,9 +122,9 @@ require (
github.com/magefile/mage v1.15.0 // @grafana/grafana-release-guild
github.com/matryer/is v1.4.0 // @grafana/grafana-as-code
github.com/mattn/go-isatty v0.0.20 // @grafana/grafana-backend-group
github.com/mattn/go-sqlite3 v1.14.19 // @grafana/grafana-backend-group
github.com/mattn/go-sqlite3 v1.14.22 // @grafana/grafana-backend-group
github.com/matttproud/golang_protobuf_extensions v1.0.4 // @grafana/alerting-backend
github.com/microsoft/go-mssqldb v1.6.1-0.20240214161942-b65008136246 // @grafana/grafana-bi-squad
github.com/microsoft/go-mssqldb v1.7.0 // @grafana/grafana-bi-squad
github.com/mitchellh/mapstructure v1.5.0 //@grafana/identity-access-team
github.com/modern-go/reflect2 v1.0.2 // @grafana/alerting-backend
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // @grafana/alerting-backend
@ -329,7 +329,6 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/asmfmt v1.3.2 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
@ -383,7 +382,7 @@ require (
github.com/segmentio/asm v1.2.0 // indirect
github.com/segmentio/encoding v0.3.6 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect
github.com/spf13/cast v1.6.0 // indirect
@ -411,7 +410,7 @@ require (
go.opentelemetry.io/otel/metric v1.26.0 // indirect
go.opentelemetry.io/proto/otlp v1.2.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap v1.27.0
golang.org/x/sys v0.21.0 // indirect
golang.org/x/term v0.21.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
@ -424,37 +423,47 @@ require (
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/kms v0.29.2 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.4 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/sqlite v1.21.2 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/libc v1.41.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.29.6 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/yaml v1.4.0 // indirect; @grafana-app-platform-squad
)
require github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20240620135321-10b6011dd787
require (
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/karlseguin/ccache/v3 v3.0.5 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/natefinch/wrap v0.2.0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/oklog/ulid/v2 v2.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pressly/goose/v3 v3.20.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sethvargo/go-retry v0.2.4 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/viper v1.18.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/mock v0.4.0 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
)
// Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream

56
go.sum
View File

@ -1394,10 +1394,10 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0/go.mod h1:Y/HgrePTmGy9HjdSGTqZNa+apUpTVIEVKXJyARP2lrk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0/go.mod h1:s1tW/At+xHqjNFvWU4G0c0Qv33KOhvbGNj0RCTQDV8s=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1/go.mod h1:c/wcGeGx5FUPbM/JltUYHZcKmigwyVLJlDq+4HdtXaw=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0 h1:yfJe15aSwEQ6Oo6J+gdfdulPNoZ3TEhmbhLIoxZcA+U=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 h1:T028gtTPiYt/RMUfs8nVsAL7FDQrfLlrm/NnRG/zcC4=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/Azure/azure-service-bus-go v0.11.5/go.mod h1:MI6ge2CuQWBVq+ly456MY7XqNLJip5LO1iSFodbNLbU=
github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck=
github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk=
@ -1526,8 +1526,9 @@ github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGn
github.com/alicebob/miniredis/v2 v2.30.1 h1:HM1rlQjq1bm9yQcsawJqSZBJ9AYgxvjkMsNtddh90+g=
github.com/alicebob/miniredis/v2 v2.30.1/go.mod h1:b25qWj4fCEsBeAAR2mlb0ufImGC6uH3VlUfb/HS5zKg=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk=
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=
@ -2333,6 +2334,8 @@ github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4 h1:t
github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4/go.mod h1:vpYI6DHvFO595rpQGooUjcyicjt9rOevldDdW79peV0=
github.com/grafana/grafana/pkg/promlib v0.0.6 h1:FuRyHMIgVVXkLuJnCflNfk3gqJflmyiI+/ZuJ9MoAfY=
github.com/grafana/grafana/pkg/promlib v0.0.6/go.mod h1:shFkrG1fQ/PPNRGhxAPNMLp0SAeG/jhqaLoG6n2191M=
github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20240620135321-10b6011dd787 h1:hWkuJda3RC3EC45GfYArB6CweHSJ7efsrJOu1db1dsE=
github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20240620135321-10b6011dd787/go.mod h1:zOInHv2y6bsgm9bIMsCVDaz1XylqIVX9r4amH4iuWPE=
github.com/grafana/grafana/pkg/util/xorm v0.0.1 h1:72QZjxWIWpSeOF8ob4aMV058kfgZyeetkAB8dmeti2o=
github.com/grafana/grafana/pkg/util/xorm v0.0.1/go.mod h1:eNfbB9f2jM8o9RfwqwjY8SYm5tvowJ8Ly+iE4P9rXII=
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
@ -2540,7 +2543,6 @@ github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCM
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY=
github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
@ -2552,7 +2554,6 @@ github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiw
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.2.1 h1:gI8os0wpRXFd4FiAY2dWiqRK037tjj3t7rKFeO4X5iw=
github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
@ -2623,7 +2624,6 @@ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/karlseguin/ccache/v3 v3.0.5 h1:hFX25+fxzNjsRlREYsoGNa2LoVEw5mPF8wkWq/UnevQ=
github.com/karlseguin/ccache/v3 v3.0.5/go.mod h1:qxC372+Qn+IBj8Pe3KvGjHPj0sWwEF7AeZVhsNPZ6uY=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@ -2750,8 +2750,8 @@ github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
@ -2759,8 +2759,8 @@ github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQth
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/microsoft/go-mssqldb v1.6.1-0.20240214161942-b65008136246 h1:KT4vTYcHqj5C5hMK5kSpyAk7MnFqfHVWLL4VqMq66S8=
github.com/microsoft/go-mssqldb v1.6.1-0.20240214161942-b65008136246/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU=
github.com/microsoft/go-mssqldb v1.7.0 h1:sgMPW0HA6Ihd37Yx0MzHyKD726C2kY/8KJsQtXHNaAs=
github.com/microsoft/go-mssqldb v1.7.0/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
@ -2868,6 +2868,8 @@ github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi
github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s=
github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@ -3154,8 +3156,9 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v1.7.1/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs=
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M=
@ -4675,14 +4678,12 @@ k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSn
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
@ -4693,11 +4694,12 @@ modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWs
modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws=
modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
@ -4713,39 +4715,41 @@ modernc.org/libc v1.20.3/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
modernc.org/libc v1.21.2/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
modernc.org/libc v1.22.4 h1:wymSbZb0AlrjdAVX3cjreCHTPCpPARbQXNz6BHPzdwQ=
modernc.org/libc v1.22.4/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4=
modernc.org/sqlite v1.18.2/go.mod h1:kvrTLEWgxUcHa2GfHBQtanR1H9ht3hTJNtKpzH9k1u0=
modernc.org/sqlite v1.21.2 h1:ixuUG0QS413Vfzyx6FWx6PYTmHaOegTY+hjzhn7L+a0=
modernc.org/sqlite v1.21.2/go.mod h1:cxbLkB5WS32DnQqeH4h4o1B0eMr8W/y8/RGuxQ3JsC0=
modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4=
modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
modernc.org/tcl v1.13.2/go.mod h1:7CLiGIPo1M8Rv1Mitpv5akc2+8fxUd2y2UzC/MfMzy0=
modernc.org/tcl v1.15.1 h1:mOQwiEK4p7HruMZcwKTZPw/aqtGM4aY00uzWhlKKYws=
modernc.org/tcl v1.15.1/go.mod h1:aEjeGJX2gz1oWKOLDVZ2tnEWLUrIn8H+GFu+akoDhqs=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=

View File

@ -1008,7 +1008,9 @@ github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38=
github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY=
github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w=
github.com/jackc/puddle v1.2.1 h1:gI8os0wpRXFd4FiAY2dWiqRK037tjj3t7rKFeO4X5iw=
github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1 h1:9Xm8CKtMZIXgcopfdWk/qZ1rt0HjMgfMR9nxxSeK6vk=
github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1/go.mod h1:zuHl3Hh+e9P6gmBPvcqR1HjkaWHC/csgyskg6IaFKFo=
github.com/jaegertracing/jaeger v1.41.0 h1:vVNky8dP46M2RjGaZ7qRENqylW+tBFay3h57N16Ip7M=

View File

@ -82,7 +82,6 @@ export interface FeatureToggles {
sqlDatasourceDatabaseSelection?: boolean;
recordedQueriesMulti?: boolean;
vizAndWidgetSplit?: boolean;
prometheusIncrementalQueryInstrumentation?: boolean;
logsExploreTableVisualisation?: boolean;
awsDatasourcesTempCredentials?: boolean;
transformationsRedesign?: boolean;
@ -180,7 +179,6 @@ export interface FeatureToggles {
disableNumericMetricsSortingInExpressions?: boolean;
grafanaManagedRecordingRules?: boolean;
queryLibrary?: boolean;
autofixDSUID?: boolean;
logsExploreTableDefaultVisualization?: boolean;
newDashboardSharingComponent?: boolean;
alertingListViewV2?: boolean;
@ -196,6 +194,7 @@ export interface FeatureToggles {
authZGRPCServer?: boolean;
openSearchBackendFlowEnabled?: boolean;
ssoSettingsLDAP?: boolean;
failWrongDSUID?: boolean;
databaseReadReplica?: boolean;
zanzana?: boolean;
}

View File

@ -139,7 +139,6 @@ export class PrometheusDatasource
this.cache = new QueryCache({
getTargetSignature: this.getPrometheusTargetSignature.bind(this),
overlapString: instanceSettings.jsonData.incrementalQueryOverlapWindow ?? defaultPrometheusQueryOverlapWindow,
profileFunction: this.getPrometheusProfileData.bind(this),
});
// This needs to be here and cannot be static because of how annotations typing affects casting of data source
@ -162,14 +161,6 @@ export class PrometheusDatasource
return query.expr;
}
getPrometheusProfileData(request: DataQueryRequest<PromQuery>, targ: PromQuery) {
return {
interval: targ.interval ?? request.interval,
expr: this.interpolateString(targ.expr),
datasource: 'Prometheus',
};
}
/**
* Get target signature for query caching
* @param request

View File

@ -6,7 +6,7 @@ import { DataFrame, DataQueryRequest, DateTime, dateTime, TimeRange } from '@gra
import { QueryEditorMode } from '../querybuilder/shared/types';
import { PromQuery } from '../types';
import { DatasourceProfileData, QueryCache } from './QueryCache';
import { QueryCache } from './QueryCache';
import { IncrementalStorageDataFrameScenarios } from './QueryCacheTestData';
// Will not interpolate vars!
@ -60,14 +60,6 @@ const mockPromRequest = (request?: Partial<DataQueryRequest<PromQuery>>): DataQu
};
};
const getPromProfileData = (request: DataQueryRequest, targ: PromQuery): DatasourceProfileData => {
return {
expr: targ.expr,
interval: targ.interval ?? request.interval,
datasource: 'prom',
};
};
describe('QueryCache: Generic', function () {
it('instantiates', () => {
const storage = new QueryCache({
@ -192,7 +184,6 @@ describe('QueryCache: Prometheus', function () {
const storage = new QueryCache<PromQuery>({
getTargetSignature: getPrometheusTargetSignature,
overlapString: '10m',
profileFunction: getPromProfileData,
});
const firstFrames = scenario.first.dataFrames as unknown as DataFrame[];
const secondFrames = scenario.second.dataFrames as unknown as DataFrame[];
@ -328,7 +319,6 @@ describe('QueryCache: Prometheus', function () {
const storage = new QueryCache<PromQuery>({
getTargetSignature: getPrometheusTargetSignature,
overlapString: '10m',
profileFunction: getPromProfileData,
});
// Initial request with all data for time range
@ -489,7 +479,6 @@ describe('QueryCache: Prometheus', function () {
const storage = new QueryCache<PromQuery>({
getTargetSignature: getPrometheusTargetSignature,
overlapString: '10m',
profileFunction: getPromProfileData,
});
const cacheRequest = storage.requestInfo(request);
expect(cacheRequest.requests[0]).toBe(request);
@ -501,7 +490,6 @@ describe('QueryCache: Prometheus', function () {
const storage = new QueryCache<PromQuery>({
getTargetSignature: getPrometheusTargetSignature,
overlapString: '10m',
profileFunction: getPromProfileData,
});
const cacheRequest = storage.requestInfo(request);
expect(cacheRequest.requests[0]).toBe(request);
@ -513,7 +501,6 @@ describe('QueryCache: Prometheus', function () {
const storage = new QueryCache<PromQuery>({
getTargetSignature: getPrometheusTargetSignature,
overlapString: '10m',
profileFunction: getPromProfileData,
});
const cacheRequest = storage.requestInfo(request);
expect(cacheRequest.requests[0]).toBe(request);

View File

@ -9,8 +9,6 @@ import {
isValidDuration,
parseDuration,
} from '@grafana/data';
import { faro } from '@grafana/faro-web-sdk';
import { config, reportInteraction } from '@grafana/runtime';
import { amendTable, Table, trimTable } from '../gcopypaste/app/features/live/data/amendTimeSeries';
import { PromQuery } from '../types';
@ -19,8 +17,6 @@ import { PromQuery } from '../types';
// (must be stable across query changes, time range changes / interval changes / panel resizes / template variable changes)
type TargetIdent = string;
type RequestID = string;
// query + template variables + interval + raw time range
// used for full target cache busting -> full range re-query
type TargetSig = string;
@ -44,22 +40,6 @@ export interface CacheRequestInfo<T extends SupportedQueryTypes> {
shouldCache: boolean;
}
export interface DatasourceProfileData {
interval?: string;
expr: string;
datasource: string;
}
interface ProfileData extends DatasourceProfileData {
identity: string;
bytes: number | null;
dashboardUID: string;
panelId?: number;
from: string;
queryRangeSeconds: number;
refreshIntervalMs: number;
}
/**
* Get field identity
* This is the string used to uniquely identify a field within a "target"
@ -76,40 +56,12 @@ export const getFieldIdent = (field: Field) => `${field.type}|${field.name}|${JS
export class QueryCache<T extends SupportedQueryTypes> {
private overlapWindowMs: number;
private getTargetSignature: (request: DataQueryRequest<T>, target: T) => string;
private getProfileData?: (request: DataQueryRequest<T>, target: T) => DatasourceProfileData;
private perfObeserver?: PerformanceObserver;
private shouldProfile: boolean;
// send profile events every 10 minutes
sendEventsInterval = 60000 * 10;
pendingRequestIdsToTargSigs = new Map<RequestID, ProfileData>();
pendingAccumulatedEvents = new Map<
string,
{
requestCount: number;
savedBytesTotal: number;
initialRequestSize: number;
lastRequestSize: number;
panelId: string;
dashId: string;
expr: string;
refreshIntervalMs: number;
sent: boolean;
datasource: string;
from: string;
queryRangeSeconds: number;
}
>();
cache = new Map<TargetIdent, TargetCache>();
constructor(options: {
getTargetSignature: (request: DataQueryRequest<T>, target: T) => string;
overlapString: string;
profileFunction?: (request: DataQueryRequest<T>, target: T) => DatasourceProfileData;
}) {
const unverifiedOverlap = options.overlapString;
if (isValidDuration(unverifiedOverlap)) {
@ -120,132 +72,9 @@ export class QueryCache<T extends SupportedQueryTypes> {
this.overlapWindowMs = durationToMilliseconds(duration);
}
if (
(config.grafanaJavascriptAgent.enabled || config.featureToggles?.prometheusIncrementalQueryInstrumentation) &&
options.profileFunction !== undefined
) {
this.profile();
this.shouldProfile = true;
} else {
this.shouldProfile = false;
}
this.getProfileData = options.profileFunction;
this.getTargetSignature = options.getTargetSignature;
}
private profile() {
// Check if PerformanceObserver is supported, and if we have Faro enabled for internal profiling
if (typeof PerformanceObserver === 'function') {
this.perfObeserver = new PerformanceObserver((list: PerformanceObserverEntryList) => {
list.getEntries().forEach((entry) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const entryTypeCast: PerformanceResourceTiming = entry as PerformanceResourceTiming;
// Safari support for this is coming in 16.4:
// https://caniuse.com/mdn-api_performanceresourcetiming_transfersize
// Gating that this exists to prevent runtime errors
const isSupported = typeof entryTypeCast?.transferSize === 'number';
if (entryTypeCast?.initiatorType === 'fetch' && isSupported) {
let fetchUrl = entryTypeCast.name;
if (fetchUrl.includes('/api/ds/query')) {
let match = fetchUrl.match(/requestId=([a-z\d]+)/i);
if (match) {
let requestId = match[1];
const requestTransferSize = Math.round(entryTypeCast.transferSize);
const currentRequest = this.pendingRequestIdsToTargSigs.get(requestId);
if (currentRequest) {
const entries = this.pendingRequestIdsToTargSigs.entries();
for (let [, value] of entries) {
if (value.identity === currentRequest.identity && value.bytes !== null) {
const previous = this.pendingAccumulatedEvents.get(value.identity);
const savedBytes = value.bytes - requestTransferSize;
this.pendingAccumulatedEvents.set(value.identity, {
datasource: value.datasource ?? 'N/A',
requestCount: (previous?.requestCount ?? 0) + 1,
savedBytesTotal: (previous?.savedBytesTotal ?? 0) + savedBytes,
initialRequestSize: value.bytes,
lastRequestSize: requestTransferSize,
panelId: currentRequest.panelId?.toString() ?? '',
dashId: currentRequest.dashboardUID ?? '',
expr: currentRequest.expr ?? '',
refreshIntervalMs: currentRequest.refreshIntervalMs ?? 0,
sent: false,
from: currentRequest.from ?? '',
queryRangeSeconds: currentRequest.queryRangeSeconds ?? 0,
});
// We don't need to save each subsequent request, only the first one
this.pendingRequestIdsToTargSigs.delete(requestId);
return;
}
}
// If we didn't return above, this should be the first request, let's save the observed size
this.pendingRequestIdsToTargSigs.set(requestId, { ...currentRequest, bytes: requestTransferSize });
}
}
}
}
});
});
this.perfObeserver.observe({ type: 'resource', buffered: false });
setInterval(this.sendPendingTrackingEvents, this.sendEventsInterval);
// Send any pending profile information when the user navigates away
window.addEventListener('beforeunload', this.sendPendingTrackingEvents);
}
}
sendPendingTrackingEvents = () => {
const entries = this.pendingAccumulatedEvents.entries();
for (let [key, value] of entries) {
if (!value.sent) {
const event = {
datasource: value.datasource.toString(),
requestCount: value.requestCount.toString(),
savedBytesTotal: value.savedBytesTotal.toString(),
initialRequestSize: value.initialRequestSize.toString(),
lastRequestSize: value.lastRequestSize.toString(),
panelId: value.panelId.toString(),
dashId: value.dashId.toString(),
expr: value.expr.toString(),
refreshIntervalMs: value.refreshIntervalMs.toString(),
from: value.from.toString(),
queryRangeSeconds: value.queryRangeSeconds.toString(),
};
if (config.featureToggles.prometheusIncrementalQueryInstrumentation) {
reportInteraction('grafana_incremental_queries_profile', event);
} else if (faro.api.pushEvent) {
faro.api.pushEvent('incremental query response size', event, 'no-interaction', {
skipDedupe: true,
});
}
this.pendingAccumulatedEvents.set(key, {
...value,
sent: true,
requestCount: 0,
savedBytesTotal: 0,
initialRequestSize: 0,
lastRequestSize: 0,
});
}
}
};
// can be used to change full range request to partial, split into multiple requests
requestInfo(request: DataQueryRequest<T>): CacheRequestInfo<T> {
// TODO: align from/to to interval to increase probability of hitting backend cache
@ -260,27 +89,11 @@ export class QueryCache<T extends SupportedQueryTypes> {
let doPartialQuery = shouldCache;
let prevTo: TimestampMs | undefined = undefined;
const refreshIntervalMs = request.intervalMs;
// pre-compute reqTargSigs
const reqTargSigs = new Map<TargetIdent, TargetSig>();
request.targets.forEach((targ) => {
let targIdent = `${request.dashboardUID}|${request.panelId}|${targ.refId}`;
let targSig = this.getTargetSignature(request, targ); // ${request.maxDataPoints} ?
if (this.shouldProfile && this.getProfileData) {
this.pendingRequestIdsToTargSigs.set(request.requestId, {
...this.getProfileData(request, targ),
identity: targIdent + '|' + targSig,
bytes: null,
panelId: request.panelId,
dashboardUID: request.dashboardUID ?? '',
from: request.rangeRaw?.from.toString() ?? '',
queryRangeSeconds: request.range.to.diff(request.range.from, 'seconds') ?? '',
refreshIntervalMs: refreshIntervalMs ?? 0,
});
}
reqTargSigs.set(targIdent, targSig);
});

View File

@ -61,6 +61,7 @@
"@react-aria/focus": "3.17.1",
"@react-aria/overlays": "3.22.1",
"@react-aria/utils": "3.24.1",
"@tanstack/react-virtual": "^3.5.1",
"ansicolor": "1.1.100",
"calculate-size": "1.1.1",
"classnames": "2.5.1",

View File

@ -1,10 +1,15 @@
import { action } from '@storybook/addon-actions';
import { Meta, StoryFn } from '@storybook/react';
import React, { useState } from 'react';
import { Meta, StoryFn, StoryObj } from '@storybook/react';
import { Chance } from 'chance';
import React, { ComponentProps, useMemo, useState } from 'react';
import { Combobox } from './Combobox';
import { Combobox, Option, Value } from './Combobox';
const meta: Meta<typeof Combobox> = {
const chance = new Chance();
type PropsAndCustomArgs = ComponentProps<typeof Combobox> & { numberOfOptions: number };
const meta: Meta<PropsAndCustomArgs> = {
title: 'Forms/Combobox',
component: Combobox,
args: {
@ -28,9 +33,11 @@ const meta: Meta<typeof Combobox> = {
],
value: 'banana',
},
render: (args) => <BasicWithState {...args} />,
};
export const Basic: StoryFn<typeof Combobox> = (args) => {
const BasicWithState: StoryFn<typeof Combobox> = (args) => {
const [value, setValue] = useState(args.value);
return (
<Combobox
@ -44,4 +51,43 @@ export const Basic: StoryFn<typeof Combobox> = (args) => {
);
};
type Story = StoryObj<typeof Combobox>;
export const Basic: Story = {};
function generateOptions(amount: number): Option[] {
return Array.from({ length: amount }, () => ({
label: chance.name(),
value: chance.guid(),
description: chance.sentence(),
}));
}
const manyOptions = generateOptions(1e5);
manyOptions.push({ label: 'Banana', value: 'banana', description: 'A yellow fruit' });
const ManyOptionsStory: StoryFn<PropsAndCustomArgs> = ({ numberOfOptions }) => {
const [value, setValue] = useState<Value>(manyOptions[5].value);
const options = useMemo(() => generateOptions(numberOfOptions), [numberOfOptions]);
return (
<Combobox
options={options}
value={value}
onChange={(val) => {
setValue(val.value);
action('onChange')(val);
}}
/>
);
};
export const ManyOptions: StoryObj<PropsAndCustomArgs> = {
args: {
numberOfOptions: 1e5,
options: undefined,
value: undefined,
},
render: ManyOptionsStory,
};
export default meta;

View File

@ -1,13 +1,17 @@
import { css } from '@emotion/css';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCombobox } from 'downshift';
import React, { useMemo, useState } from 'react';
import React, { useMemo, useRef, useState } from 'react';
import { useStyles2 } from '../../themes';
import { Icon } from '../Icon/Icon';
import { Input, Props as InputProps } from '../Input/Input';
type Value = string | number;
type Option = {
export type Value = string | number;
export type Option = {
label: string;
value: Value;
description?: string;
};
interface ComboboxProps
@ -33,32 +37,86 @@ function itemFilter(inputValue: string) {
};
}
function estimateSize() {
return 60;
}
export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxProps) => {
const [items, setItems] = useState(options);
const selectedItem = useMemo(() => options.find((option) => option.value === value) || null, [options, value]);
const listRef = useRef(null);
const styles = useStyles2(getStyles);
const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => listRef.current,
estimateSize,
overscan: 2,
});
const { getInputProps, getMenuProps, getItemProps, isOpen } = useCombobox({
items,
itemToString,
selectedItem,
scrollIntoView: () => {},
onInputValueChange: ({ inputValue }) => {
setItems(options.filter(itemFilter(inputValue)));
},
onSelectedItemChange: ({ selectedItem }) => onChange(selectedItem),
onHighlightedIndexChange: ({ highlightedIndex, type }) => {
if (type !== useCombobox.stateChangeTypes.MenuMouseLeave) {
rowVirtualizer.scrollToIndex(highlightedIndex);
}
},
});
return (
<div>
<Input suffix={<Icon name={isOpen ? 'search' : 'angle-down'} />} {...restProps} {...getInputProps()} />
<ul {...getMenuProps()}>
{isOpen &&
items.map((item, index) => {
return (
<li key={item.value} {...getItemProps({ item, index })}>
{item.label}
</li>
);
})}
</ul>
<div className={styles.dropdown} {...getMenuProps({ ref: listRef })}>
{isOpen && (
<ul style={{ height: rowVirtualizer.getTotalSize() }}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
return (
<li
key={items[virtualRow.index].value}
{...getItemProps({ item: items[virtualRow.index], index: virtualRow.index })}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
className={styles.menuItem}
style={{
transform: `translateY(${virtualRow.start}px)`,
}}
>
<span>{items[virtualRow.index].label}</span>
{items[virtualRow.index].description && <span>{items[virtualRow.index].description}</span>}
</li>
);
})}
</ul>
)}
</div>
</div>
);
};
const getStyles = () => ({
dropdown: css({
position: 'absolute',
height: 400,
width: 600,
overflowY: 'scroll',
contain: 'strict',
}),
menuItem: css({
position: 'absolute',
top: 0,
left: 0,
width: '100%',
display: 'flex',
flexDirection: 'column',
'&:first-child': {
fontWeight: 'bold',
},
}),
});

View File

@ -135,6 +135,7 @@ export function PanelChrome({
const theme = useTheme2();
const styles = useStyles2(getStyles);
const panelContentId = useId();
const panelTitleId = useId().replace(/:/g, '_');
const hasHeader = !hoverHeader;
@ -179,7 +180,13 @@ export function PanelChrome({
{/* Non collapsible title */}
{!collapsible && title && (
<div className={styles.title}>
<Text element="h2" variant="h6" truncate title={typeof title === 'string' ? title : undefined}>
<Text
element="h2"
variant="h6"
truncate
title={typeof title === 'string' ? title : undefined}
id={panelTitleId}
>
{title}
</Text>
</div>
@ -206,7 +213,7 @@ export function PanelChrome({
aria-hidden={!!title}
aria-label={!title ? 'toggle collapse panel' : undefined}
/>
<Text variant="h6" truncate>
<Text variant="h6" truncate id={panelTitleId}>
{title}
</Text>
</button>
@ -249,6 +256,7 @@ export function PanelChrome({
<section
className={cx(styles.container, { [styles.transparentContainer]: isPanelTransparent })}
style={containerStyles}
aria-labelledby={!!title ? panelTitleId : undefined}
data-testid={testid}
tabIndex={0} // eslint-disable-line jsx-a11y/no-noninteractive-tabindex
onFocus={onFocus}

View File

@ -14,6 +14,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/version"
openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
"k8s.io/apiserver/pkg/registry/generic"
@ -99,6 +100,8 @@ func SetupConfig(
handler = filters.WithAcceptHeader(handler)
handler = filters.WithPathRewriters(handler, pathRewriters)
handler = k8stracing.WithTracing(handler, serverConfig.TracerProvider, "KubernetesAPI")
// Configure filters.WithPanicRecovery to not crash on panic
utilruntime.ReallyCrash = false
return handler
}

View File

@ -21,25 +21,23 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
type ZanzanaClient interface{}
// ProvideZanzana used to register ZanzanaClient.
// It will also start an embedded ZanzanaSever if mode is set to "embedded".
func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureToggles) (ZanzanaClient, error) {
func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureToggles) (zanzana.Client, error) {
if !features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
return zanzana.NoopClient{}, nil
}
logger := log.New("zanzana")
var client *zanzana.Client
var client zanzana.Client
switch cfg.Zanzana.Mode {
case setting.ZanzanaModeClient:
conn, err := grpc.NewClient(cfg.Zanzana.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, fmt.Errorf("failed to create zanzana client to remote server: %w", err)
}
client = zanzana.NewClient(openfgav1.NewOpenFGAServiceClient(conn))
client = zanzana.NewClient(conn)
case setting.ZanzanaModeEmbedded:
store, err := zanzana.NewEmbeddedStore(cfg, db, logger)
if err != nil {
@ -53,7 +51,7 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureTogg
channel := &inprocgrpc.Channel{}
openfgav1.RegisterOpenFGAServiceServer(channel, srv)
client = zanzana.NewClient(openfgav1.NewOpenFGAServiceClient(channel))
client = zanzana.NewClient(channel)
default:
return nil, fmt.Errorf("unsupported zanzana mode: %s", cfg.Zanzana.Mode)
}

View File

@ -1,16 +1,46 @@
package zanzana
import (
"context"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"google.golang.org/grpc"
"github.com/grafana/grafana/pkg/infra/log"
)
// FIXME(kalleep): Build out our wrapper client for openFGA
type Client struct {
c openfgav1.OpenFGAServiceClient
// Client is a wrapper around OpenFGAServiceClient with only methods using in Grafana included.
type Client interface {
Check(ctx context.Context, in *openfgav1.CheckRequest, opts ...grpc.CallOption) (*openfgav1.CheckResponse, error)
ListObjects(ctx context.Context, in *openfgav1.ListObjectsRequest, opts ...grpc.CallOption) (*openfgav1.ListObjectsResponse, error)
}
func NewClient(c openfgav1.OpenFGAServiceClient) *Client {
return &Client{c}
type zanzanaClient struct {
client openfgav1.OpenFGAServiceClient
logger log.Logger
}
func NewClient(cc grpc.ClientConnInterface) Client {
return &zanzanaClient{
client: openfgav1.NewOpenFGAServiceClient(cc),
logger: log.New("zanzana-client"),
}
}
func (c *zanzanaClient) Check(ctx context.Context, in *openfgav1.CheckRequest, opts ...grpc.CallOption) (*openfgav1.CheckResponse, error) {
return c.client.Check(ctx, in, opts...)
}
func (c *zanzanaClient) ListObjects(ctx context.Context, in *openfgav1.ListObjectsRequest, opts ...grpc.CallOption) (*openfgav1.ListObjectsResponse, error) {
return c.client.ListObjects(ctx, in, opts...)
}
type NoopClient struct{}
func (nc NoopClient) Check(ctx context.Context, in *openfgav1.CheckRequest, opts ...grpc.CallOption) (*openfgav1.CheckResponse, error) {
return nil, nil
}
func (nc NoopClient) ListObjects(ctx context.Context, in *openfgav1.ListObjectsRequest, opts ...grpc.CallOption) (*openfgav1.ListObjectsResponse, error) {
return nil, nil
}

View File

@ -18,4 +18,5 @@ var (
ErrDataSourceNameInvalid = errutil.ValidationFailed("datasource.nameInvalid", errutil.WithPublicMessage("Invalid datasource name."))
ErrDataSourceURLInvalid = errutil.ValidationFailed("datasource.urlInvalid", errutil.WithPublicMessage("Invalid datasource url."))
ErrDataSourceAPIVersionInvalid = errutil.ValidationFailed("datasource.apiVersionInvalid", errutil.WithPublicMessage("Invalid datasource apiVersion."))
ErrDataSourceUIDInvalid = errutil.ValidationFailed("datasource.uidInvalid", errutil.WithPublicMessage("Invalid datasource UID."))
)

View File

@ -252,8 +252,8 @@ func (ss *SqlStore) AddDataSource(ctx context.Context, cmd *datasources.AddDataS
cmd.UID = uid
} else if err := util.ValidateUID(cmd.UID); err != nil {
logDeprecatedInvalidDsUid(ss.logger, cmd.UID, cmd.Name, "create", err)
if ss.features != nil && ss.features.IsEnabled(ctx, featuremgmt.FlagAutofixDSUID) {
return fmt.Errorf("invalid UID for datasource %s: %w", cmd.Name, err)
if ss.features != nil && ss.features.IsEnabled(ctx, featuremgmt.FlagFailWrongDSUID) {
return datasources.ErrDataSourceUIDInvalid.Errorf("invalid UID for datasource %s: %w", cmd.Name, err)
}
}
@ -329,8 +329,8 @@ func (ss *SqlStore) UpdateDataSource(ctx context.Context, cmd *datasources.Updat
if cmd.UID != "" {
if err := util.ValidateUID(cmd.UID); err != nil {
logDeprecatedInvalidDsUid(ss.logger, cmd.UID, cmd.Name, "update", err)
if ss.features != nil && ss.features.IsEnabled(ctx, featuremgmt.FlagAutofixDSUID) {
cmd.UID = util.AutofixUID(cmd.UID)
if ss.features != nil && ss.features.IsEnabled(ctx, featuremgmt.FlagFailWrongDSUID) {
return datasources.ErrDataSourceUIDInvalid.Errorf("invalid UID for datasource %s: %w", cmd.Name, err)
}
}
}

View File

@ -104,7 +104,7 @@ func TestIntegrationDataAccess(t *testing.T) {
ss := SqlStore{
db: db,
logger: log.NewNopLogger(),
features: featuremgmt.WithFeatures(featuremgmt.FlagAutofixDSUID),
features: featuremgmt.WithFeatures(featuremgmt.FlagFailWrongDSUID),
}
cmd := defaultAddDatasourceCommand
cmd.UID = "test/uid"
@ -232,28 +232,21 @@ func TestIntegrationDataAccess(t *testing.T) {
require.NoError(t, err)
})
t.Run("updates UID with a valid one", func(t *testing.T) {
t.Run("fails to update a datasource with an invalid uid", func(t *testing.T) {
db := db.InitTestDB(t)
ds := initDatasource(db)
ss := SqlStore{
db: db,
logger: log.NewNopLogger(),
features: featuremgmt.WithFeatures(featuremgmt.FlagAutofixDSUID),
features: featuremgmt.WithFeatures(featuremgmt.FlagFailWrongDSUID),
}
require.NotEmpty(t, ds.UID)
cmd := defaultUpdateDatasourceCommand
cmd.ID = ds.ID
cmd.UID = "new/uid"
res, err := ss.UpdateDataSource(context.Background(), &cmd)
require.NoError(t, err)
require.Equal(t, "new-uid", res.UID)
// Return the datasource with the valid UID
query := datasources.GetDataSourceQuery{UID: "new-uid", OrgID: 10}
dataSource, err := ss.GetDataSource(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, "new-uid", dataSource.UID)
_, err := ss.UpdateDataSource(context.Background(), &cmd)
require.ErrorContains(t, err, "invalid format of UID")
})
})

View File

@ -480,13 +480,6 @@ var (
FrontendOnly: true,
Owner: grafanaDashboardsSquad,
},
{
Name: "prometheusIncrementalQueryInstrumentation",
Description: "Adds RudderStack events to incremental queries",
FrontendOnly: true,
Stage: FeatureStageExperimental,
Owner: grafanaObservabilityMetricsSquad,
},
{
Name: "logsExploreTableVisualisation",
Description: "A table visualisation for logs in Explore",
@ -1222,12 +1215,6 @@ var (
FrontendOnly: false,
AllowSelfServe: false,
},
{
Name: "autofixDSUID",
Description: "Automatically migrates invalid datasource UIDs",
Stage: FeatureStageExperimental,
Owner: grafanaPluginsPlatformSquad,
},
{
Name: "logsExploreTableDefaultVisualization",
Description: "Sets the logs table as default visualisation in logs explore",
@ -1336,6 +1323,12 @@ var (
HideFromDocs: true,
HideFromAdminPage: true,
},
{
Name: "failWrongDSUID",
Description: "Throws an error if a datasource has an invalid UIDs",
Stage: FeatureStageExperimental,
Owner: grafanaPluginsPlatformSquad,
},
{
Name: "databaseReadReplica",
Description: "Use a read replica for some database queries.",

View File

@ -63,7 +63,6 @@ frontendSandboxMonitorOnly,experimental,@grafana/plugins-platform-backend,false,
sqlDatasourceDatabaseSelection,preview,@grafana/dataviz-squad,false,false,true
recordedQueriesMulti,GA,@grafana/observability-metrics,false,false,false
vizAndWidgetSplit,experimental,@grafana/dashboards-squad,false,false,true
prometheusIncrementalQueryInstrumentation,experimental,@grafana/observability-metrics,false,false,true
logsExploreTableVisualisation,GA,@grafana/observability-logs,false,false,true
awsDatasourcesTempCredentials,experimental,@grafana/aws-datasources,false,false,false
transformationsRedesign,GA,@grafana/observability-metrics,false,false,true
@ -161,7 +160,6 @@ accessActionSets,experimental,@grafana/identity-access-team,false,false,false
disableNumericMetricsSortingInExpressions,experimental,@grafana/observability-metrics,false,true,false
grafanaManagedRecordingRules,experimental,@grafana/alerting-squad,false,false,false
queryLibrary,experimental,@grafana/explore-squad,false,false,false
autofixDSUID,experimental,@grafana/plugins-platform-backend,false,false,false
logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true
newDashboardSharingComponent,experimental,@grafana/sharing-squad,false,false,true
alertingListViewV2,experimental,@grafana/alerting-squad,false,false,true
@ -177,5 +175,6 @@ pinNavItems,experimental,@grafana/grafana-frontend-platform,false,false,false
authZGRPCServer,experimental,@grafana/identity-access-team,false,false,false
openSearchBackendFlowEnabled,preview,@grafana/aws-datasources,false,false,false
ssoSettingsLDAP,experimental,@grafana/identity-access-team,false,false,false
failWrongDSUID,experimental,@grafana/plugins-platform-backend,false,false,false
databaseReadReplica,experimental,@grafana/grafana-backend-services-squad,false,false,false
zanzana,experimental,@grafana/identity-access-team,false,false,false

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
63 sqlDatasourceDatabaseSelection preview @grafana/dataviz-squad false false true
64 recordedQueriesMulti GA @grafana/observability-metrics false false false
65 vizAndWidgetSplit experimental @grafana/dashboards-squad false false true
prometheusIncrementalQueryInstrumentation experimental @grafana/observability-metrics false false true
66 logsExploreTableVisualisation GA @grafana/observability-logs false false true
67 awsDatasourcesTempCredentials experimental @grafana/aws-datasources false false false
68 transformationsRedesign GA @grafana/observability-metrics false false true
160 disableNumericMetricsSortingInExpressions experimental @grafana/observability-metrics false true false
161 grafanaManagedRecordingRules experimental @grafana/alerting-squad false false false
162 queryLibrary experimental @grafana/explore-squad false false false
autofixDSUID experimental @grafana/plugins-platform-backend false false false
163 logsExploreTableDefaultVisualization experimental @grafana/observability-logs false false true
164 newDashboardSharingComponent experimental @grafana/sharing-squad false false true
165 alertingListViewV2 experimental @grafana/alerting-squad false false true
175 authZGRPCServer experimental @grafana/identity-access-team false false false
176 openSearchBackendFlowEnabled preview @grafana/aws-datasources false false false
177 ssoSettingsLDAP experimental @grafana/identity-access-team false false false
178 failWrongDSUID experimental @grafana/plugins-platform-backend false false false
179 databaseReadReplica experimental @grafana/grafana-backend-services-squad false false false
180 zanzana experimental @grafana/identity-access-team false false false

View File

@ -263,10 +263,6 @@ const (
// Split panels between visualizations and widgets
FlagVizAndWidgetSplit = "vizAndWidgetSplit"
// FlagPrometheusIncrementalQueryInstrumentation
// Adds RudderStack events to incremental queries
FlagPrometheusIncrementalQueryInstrumentation = "prometheusIncrementalQueryInstrumentation"
// FlagLogsExploreTableVisualisation
// A table visualisation for logs in Explore
FlagLogsExploreTableVisualisation = "logsExploreTableVisualisation"
@ -655,10 +651,6 @@ const (
// Enables Query Library feature in Explore
FlagQueryLibrary = "queryLibrary"
// FlagAutofixDSUID
// Automatically migrates invalid datasource UIDs
FlagAutofixDSUID = "autofixDSUID"
// FlagLogsExploreTableDefaultVisualization
// Sets the logs table as default visualisation in logs explore
FlagLogsExploreTableDefaultVisualization = "logsExploreTableDefaultVisualization"
@ -719,6 +711,10 @@ const (
// Use the new SSO Settings API to configure LDAP
FlagSsoSettingsLDAP = "ssoSettingsLDAP"
// FlagFailWrongDSUID
// Throws an error if a datasource has an invalid UIDs
FlagFailWrongDSUID = "failWrongDSUID"
// FlagDatabaseReadReplica
// Use a read replica for some database queries.
FlagDatabaseReadReplica = "databaseReadReplica"

View File

@ -401,8 +401,9 @@
{
"metadata": {
"name": "autofixDSUID",
"resourceVersion": "1718727528075",
"creationTimestamp": "2024-05-03T11:32:07Z"
"resourceVersion": "1717578796182",
"creationTimestamp": "2024-05-03T11:32:07Z",
"deletionTimestamp": "2024-06-18T14:28:32Z"
},
"spec": {
"description": "Automatically migrates invalid datasource UIDs",
@ -915,6 +916,18 @@
"frontend": true
}
},
{
"metadata": {
"name": "failWrongDSUID",
"resourceVersion": "1718721033692",
"creationTimestamp": "2024-06-18T14:30:33Z"
},
"spec": {
"description": "Throws an error if a datasource has an invalid UIDs",
"stage": "experimental",
"codeowner": "@grafana/plugins-platform-backend"
}
},
{
"metadata": {
"name": "faroDatasourceSelector",
@ -1819,7 +1832,8 @@
"metadata": {
"name": "prometheusIncrementalQueryInstrumentation",
"resourceVersion": "1718727528075",
"creationTimestamp": "2023-07-05T19:39:49Z"
"creationTimestamp": "2023-07-05T19:39:49Z",
"deletionTimestamp": "2024-06-20T11:30:37Z"
},
"spec": {
"description": "Adds RudderStack events to incremental queries",

View File

@ -5,7 +5,6 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/store"
)
@ -22,8 +21,8 @@ type FakeRuleService struct {
AuthorizeDatasourceAccessForRuleGroupFunc func(context.Context, identity.Requester, models.RulesGroup) error
HasAccessToRuleGroupFunc func(context.Context, identity.Requester, models.RulesGroup) (bool, error)
AuthorizeAccessToRuleGroupFunc func(context.Context, identity.Requester, models.RulesGroup) error
HasAccessInFolderFunc func(context.Context, identity.Requester, accesscontrol.Namespaced) (bool, error)
AuthorizeAccessInFolderFunc func(context.Context, identity.Requester, accesscontrol.Namespaced) error
HasAccessInFolderFunc func(context.Context, identity.Requester, models.Namespaced) (bool, error)
AuthorizeAccessInFolderFunc func(context.Context, identity.Requester, models.Namespaced) error
AuthorizeRuleChangesFunc func(context.Context, identity.Requester, *store.GroupDelta) error
Calls []Call
@ -77,7 +76,7 @@ func (s *FakeRuleService) AuthorizeAccessToRuleGroup(ctx context.Context, user i
return nil
}
func (s *FakeRuleService) HasAccessInFolder(ctx context.Context, user identity.Requester, namespaced accesscontrol.Namespaced) (bool, error) {
func (s *FakeRuleService) HasAccessInFolder(ctx context.Context, user identity.Requester, namespaced models.Namespaced) (bool, error) {
s.Calls = append(s.Calls, Call{"HasAccessInFolder", []interface{}{ctx, user, namespaced}})
if s.HasAccessInFolderFunc != nil {
return s.HasAccessInFolderFunc(ctx, user, namespaced)
@ -85,7 +84,7 @@ func (s *FakeRuleService) HasAccessInFolder(ctx context.Context, user identity.R
return false, nil
}
func (s *FakeRuleService) AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, namespaced accesscontrol.Namespaced) error {
func (s *FakeRuleService) AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error {
s.Calls = append(s.Calls, Call{"AuthorizeAccessInFolder", []interface{}{ctx, user, namespaced}})
if s.AuthorizeAccessInFolderFunc != nil {
return s.AuthorizeAccessInFolderFunc(ctx, user, namespaced)

View File

@ -31,10 +31,6 @@ func NewRuleService(ac accesscontrol.AccessControl) *RuleService {
}
}
type Namespaced interface {
GetNamespaceUID() string
}
// getReadFolderAccessEvaluator constructs accesscontrol.Evaluator that checks all permissions required to read rules in specific folder
func getReadFolderAccessEvaluator(folderUID string) accesscontrol.Evaluator {
return accesscontrol.EvalAll(
@ -130,7 +126,7 @@ func (r *RuleService) AuthorizeAccessToRuleGroup(ctx context.Context, user ident
// - ("folders:read") read the folder
// - ("alert.rules:read") read alert rules in the folder
// Returns false if the requester does not have enough permissions, and error if something went wrong during the permission evaluation.
func (r *RuleService) HasAccessInFolder(ctx context.Context, user identity.Requester, rule Namespaced) (bool, error) {
func (r *RuleService) HasAccessInFolder(ctx context.Context, user identity.Requester, rule models.Namespaced) (bool, error) {
eval := accesscontrol.EvalAll(getReadFolderAccessEvaluator(rule.GetNamespaceUID()))
return r.HasAccess(ctx, user, eval)
}
@ -140,7 +136,7 @@ func (r *RuleService) HasAccessInFolder(ctx context.Context, user identity.Reque
// - ("folders:read") read the folder
// - ("alert.rules:read") read alert rules in the folder
// Returns error if at least one permission is missing or if something went wrong during the permission evaluation
func (r *RuleService) AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, rule Namespaced) error {
func (r *RuleService) AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, rule models.Namespaced) error {
eval := accesscontrol.EvalAll(getReadFolderAccessEvaluator(rule.GetNamespaceUID()))
return r.HasAccessOrError(ctx, user, eval, func() string {
return fmt.Sprintf("access rules in folder '%s'", rule.GetNamespaceUID())

View File

@ -45,7 +45,7 @@ type RuleAccessControlService interface {
AuthorizeRuleChanges(ctx context.Context, user identity.Requester, change *store.GroupDelta) error
AuthorizeDatasourceAccessForRule(ctx context.Context, user identity.Requester, rule *models.AlertRule) error
AuthorizeDatasourceAccessForRuleGroup(ctx context.Context, user identity.Requester, rules models.RulesGroup) error
AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, namespaced accesscontrol.Namespaced) error
AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error
}
// API handlers.

View File

@ -11,7 +11,6 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/state"
@ -147,7 +146,7 @@ func (f fakeRuleAccessControlService) AuthorizeAccessToRuleGroup(ctx context.Con
return nil
}
func (f fakeRuleAccessControlService) AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, namespaced accesscontrol.Namespaced) error {
func (f fakeRuleAccessControlService) AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error {
return nil
}

View File

@ -269,6 +269,11 @@ type AlertRule struct {
NotificationSettings []NotificationSettings `xorm:"notification_settings"` // we use slice to workaround xorm mapping that does not serialize a struct to JSON unless it's a slice
}
// Namespaced describes a class of resources that are stored in a specific namespace.
type Namespaced interface {
GetNamespaceUID() string
}
// AlertRuleWithOptionals This is to avoid having to pass in additional arguments deep in the call stack. Alert rule
// object is created in an early validation step without knowledge about current alert rule fields or if they need to be
// overridden. This is done in a later step and, in that step, we did not have knowledge about if a field was optional

View File

@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
@ -24,7 +23,7 @@ type SilenceService struct {
}
type RuleAccessControlService interface {
HasAccessInFolder(ctx context.Context, user identity.Requester, rule accesscontrol.Namespaced) (bool, error)
HasAccessInFolder(ctx context.Context, user identity.Requester, rule models.Namespaced) (bool, error)
}
// SilenceAccessControlService provides access control for silences.

View File

@ -15,7 +15,6 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/org"
@ -61,7 +60,7 @@ func TestWithRuleMetadata(t *testing.T) {
user := ac.BackgroundUser("test", 1, org.RoleNone, nil)
t.Run("Attach rule metadata to silences", func(t *testing.T) {
ruleAuthz := fakes.FakeRuleService{}
ruleAuthz.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, silence accesscontrol.Namespaced) (bool, error) {
ruleAuthz.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, silence models.Namespaced) (bool, error) {
return true, nil
}
@ -95,7 +94,7 @@ func TestWithRuleMetadata(t *testing.T) {
})
t.Run("Don't attach full rule metadata if no access or global", func(t *testing.T) {
ruleAuthz := fakes.FakeRuleService{}
ruleAuthz.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, silence accesscontrol.Namespaced) (bool, error) {
ruleAuthz.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, silence models.Namespaced) (bool, error) {
return silence.GetNamespaceUID() == "folder1", nil
}
@ -134,7 +133,7 @@ func TestWithRuleMetadata(t *testing.T) {
})
t.Run("Don't check same namespace access more than once", func(t *testing.T) {
ruleAuthz := fakes.FakeRuleService{}
ruleAuthz.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, silence accesscontrol.Namespaced) (bool, error) {
ruleAuthz.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, silence models.Namespaced) (bool, error) {
return true, nil
}
@ -159,7 +158,7 @@ func TestWithRuleMetadata(t *testing.T) {
require.NoError(t, svc.WithRuleMetadata(context.Background(), user, silencesWithMetadata...))
assert.Lenf(t, ruleAuthz.Calls, 1, "HasAccessInFolder should be called only once per namespace")
assert.Equal(t, "HasAccessInFolder", ruleAuthz.Calls[0].MethodName)
assert.Equal(t, "folder1", ruleAuthz.Calls[0].Arguments[2].(accesscontrol.Namespaced).GetNamespaceUID())
assert.Equal(t, "folder1", ruleAuthz.Calls[0].Arguments[2].(models.Namespaced).GetNamespaceUID())
})
}

View File

@ -5,7 +5,6 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/store"
)
@ -13,7 +12,7 @@ import (
type RuleAccessControlService interface {
HasAccess(ctx context.Context, user identity.Requester, evaluator ac.Evaluator) (bool, error)
AuthorizeAccessToRuleGroup(ctx context.Context, user identity.Requester, rules models.RulesGroup) error
AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, namespaced accesscontrol.Namespaced) error
AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error
AuthorizeRuleChanges(ctx context.Context, user identity.Requester, change *store.GroupDelta) error
}

View File

@ -11,7 +11,6 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/accesscontrol"
accesscontrol2 "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/store"
@ -200,7 +199,7 @@ func TestAuthorizeAccessToRule(t *testing.T) {
rs.HasAccessFunc = func(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) {
return false, nil
}
rs.AuthorizeAccessInFolderFunc = func(ctx context.Context, requester identity.Requester, namespaced accesscontrol2.Namespaced) error {
rs.AuthorizeAccessInFolderFunc = func(ctx context.Context, requester identity.Requester, namespaced models.Namespaced) error {
return nil
}
@ -231,7 +230,7 @@ func TestAuthorizeAccessToRule(t *testing.T) {
return false, nil
}
expected = errors.New("test2")
rs.AuthorizeAccessInFolderFunc = func(ctx context.Context, requester identity.Requester, rule accesscontrol2.Namespaced) error {
rs.AuthorizeAccessInFolderFunc = func(ctx context.Context, requester identity.Requester, rule models.Namespaced) error {
return expected
}

View File

@ -1020,7 +1020,7 @@ func TestGetAlertRule(t *testing.T) {
service, _, _, ac := initServiceWithData(t)
expected := errors.New("test")
ac.AuthorizeAccessInFolderFunc = func(ctx context.Context, user identity.Requester, namespaced accesscontrol.Namespaced) error {
ac.AuthorizeAccessInFolderFunc = func(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error {
assert.Equal(t, u, user)
assert.EqualValues(t, rule, namespaced)
return expected
@ -1034,7 +1034,7 @@ func TestGetAlertRule(t *testing.T) {
assert.Equal(t, "AuthorizeRuleRead", ac.Calls[0].Method)
ac.Calls = nil
ac.AuthorizeAccessInFolderFunc = func(ctx context.Context, user identity.Requester, namespaced accesscontrol.Namespaced) error {
ac.AuthorizeAccessInFolderFunc = func(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error {
return nil
}

View File

@ -9,7 +9,6 @@ import (
mock "github.com/stretchr/testify/mock"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/services/ngalert/store"
@ -161,7 +160,7 @@ type fakeRuleAccessControlService struct {
mu sync.Mutex
Calls []call
AuthorizeAccessToRuleGroupFunc func(ctx context.Context, user identity.Requester, rules models.RulesGroup) error
AuthorizeAccessInFolderFunc func(ctx context.Context, user identity.Requester, namespaced accesscontrol.Namespaced) error
AuthorizeAccessInFolderFunc func(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error
AuthorizeRuleChangesFunc func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error
CanReadAllRulesFunc func(ctx context.Context, user identity.Requester) (bool, error)
CanWriteAllRulesFunc func(ctx context.Context, user identity.Requester) (bool, error)

View File

@ -109,7 +109,7 @@ type broadcaster[T any] struct {
// subscription management
cache Cache[T]
cache channelCache[T]
subscribe chan chan T
unsubscribe chan (<-chan T)
subs map[<-chan T]chan T
@ -166,7 +166,7 @@ func (b *broadcaster[T]) init(ctx context.Context, connect ConnectFunc[T]) error
// initialize our internal state
b.shouldTerminate = ctx.Done()
b.cache = NewCache[T](ctx, 100)
b.cache = newChannelCache[T](ctx, 100)
b.subscribe = make(chan chan T, 100)
b.unsubscribe = make(chan (<-chan T), 100)
b.subs = make(map[<-chan T]chan T)
@ -239,9 +239,9 @@ func (b *broadcaster[T]) stream(input <-chan T) {
}
}
const DefaultCacheSize = 100
const defaultCacheSize = 100
type Cache[T any] interface {
type channelCache[T any] interface {
Len() int
Add(item T)
Get(i int) T
@ -260,12 +260,12 @@ type cache[T any] struct {
ctx context.Context
}
func NewCache[T any](ctx context.Context, size int) Cache[T] {
func newChannelCache[T any](ctx context.Context, size int) channelCache[T] {
c := &cache[T]{}
c.ctx = ctx
if size <= 0 {
size = DefaultCacheSize
size = defaultCacheSize
}
c.size = size
c.cache = make([]T, c.size)

View File

@ -8,7 +8,7 @@ import (
)
func TestCache(t *testing.T) {
c := NewCache[int](context.Background(), 10)
c := newChannelCache[int](context.Background(), 10)
e := []int{}
err := c.Range(func(i int) error {

View File

@ -1,92 +0,0 @@
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
cloud.google.com/go/auth v0.2.2 h1:gmxNJs4YZYcw6YvKRtVBaF2fyUE6UrWPyzU8jHvYfmI=
cloud.google.com/go/auth/oauth2adapt v0.2.1 h1:VSPmMmUlT8CkIZ2PzD9AlLN+R3+D1clXMWHHa6vG/Ag=
cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc=
cloud.google.com/go/storage v1.38.0 h1:Az68ZRGlnNTpIBbLjSMIV2BDcwwXYlRlQzis0llkpJg=
github.com/aws/aws-sdk-go v1.51.31 h1:4TM+sNc+Dzs7wY1sJ0+J8i60c6rkgnKP1pvPx8ghsSY=
github.com/aws/aws-sdk-go-v2 v1.16.2 h1:fqlCk6Iy3bnCumtrLz9r3mJ/2gUT0pJ0wLFVIdWh+JA=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1 h1:SdK4Ppk5IzLs64ZMvr6MrSficMtjY2oS0WOORXTlxwU=
github.com/aws/aws-sdk-go-v2/config v1.15.3 h1:5AlQD0jhVXlGzwo+VORKiUuogkG7pQcLJNzIzK7eodw=
github.com/aws/aws-sdk-go-v2/credentials v1.11.2 h1:RQQ5fzclAKJyY5TvF+fkjJEwzK4hnxQCLOu5JXzDmQo=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3 h1:LWPg5zjHV9oz/myQr4wMs0gi4CjnDN/ILmyZUFYXZsU=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3 h1:ir7iEq78s4txFGgwcLqD6q9IIPzTQNRJXulJd9h/zQo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9 h1:onz/VaaxZ7Z4V+WIN9Txly9XLTmoOh1oJ8XcAC3pako=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3 h1:9stUQR/u2KXU6HkFJYlqnZEjBnbgrVbG6I5HN09xZh0=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10 h1:by9P+oy3P/CwggN4ClnW2D4oL91QV7pBzBICi1chZvQ=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1 h1:T4pFel53bkHjL2mMo+4DKE6r6AuoZnM0fg7k1/ratr4=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3 h1:I0dcwWitE752hVSMrsLCxqNQ+UdEp3nACx2bYNMQq+k=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3 h1:Gh1Gpyh01Yvn7ilO/b/hr01WgNpaszfbKMUgqM186xQ=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3 h1:BKjwCJPnANbkwQ8vzSbaZDKawwagDubrH/z/c0X+kbQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3 h1:rMPtwA7zzkSQZhhz9U3/SoIDz/NZ7Q+iRn4EIO8rSyU=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.3 h1:frW4ikGcxfAEDfmQqWgMLp+F1n4nRo9sF39OcIb5BkQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.3 h1:cJGRyzCSVwZC7zZZ1xbx9m32UnrKydRYhOvcD1NYP9Q=
github.com/aws/smithy-go v1.11.2 h1:eG/N+CcUMAvsdffgMvjMKwfyDzIkjM6pfxMJ8Mzc6mE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/fullstorydev/grpchan v1.1.1 h1:heQqIJlAv5Cnks9a70GRL2EJke6QQoUB25VGR6TZQas=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240613114114-5e2f08de316d h1:/UE5JdF+0hxll7EuuO7zRzAxXrvAxQo5M9eqOepc2mQ=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
gocloud.dev v0.25.0 h1:Y7vDq8xj7SyM848KXf32Krda2e6jQ4CLh/mTeCSqXtk=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
google.golang.org/api v0.176.0 h1:dHj1/yv5Dm/eQTXiP9hNCRT3xzJHWXeNdRq29XbMxoE=
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 h1:+rdxYoE3E5htTEWIe15GlN6IfvbURM//Jt0mmkmm6ZU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU=
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=

View File

@ -32,7 +32,6 @@ var mtx sync.Mutex
// Legacy UID pattern
var validUIDCharPattern = `a-zA-Z0-9\-\_`
var validUIDPattern = regexp.MustCompile(`^[` + validUIDCharPattern + `]*$`).MatchString
var validUIDReplacer = regexp.MustCompile(`[^` + validUIDCharPattern + `]`).ReplaceAllString
// IsValidShortUID checks if short unique identifier contains valid characters
// NOTE: future Grafana UIDs will need conform to https://github.com/kubernetes/apimachinery/blob/master/pkg/util/validation/validation.go#L43
@ -93,13 +92,3 @@ func ValidateUID(uid string) error {
}
return nil
}
func AutofixUID(uid string) string {
if IsShortUIDTooLong(uid) {
return uid[:MaxUIDLength]
}
if !IsValidShortUID(uid) {
uid = validUIDReplacer(uid, "-")
}
return uid
}

View File

@ -143,32 +143,3 @@ func TestValidateUID(t *testing.T) {
})
}
}
func TestAutofixUID(t *testing.T) {
var tests = []struct {
name string
uid string
expected string
}{
{
name: "return input when input is valid",
uid: "f8cc010c-ee72-4681-89d2-d46e1bd47d33",
expected: "f8cc010c-ee72-4681-89d2-d46e1bd47d33",
},
{
name: "generate new uid when input is too long",
uid: strings.Repeat("1", MaxUIDLength+1),
expected: strings.Repeat("1", MaxUIDLength),
},
{
name: "generate new uid when input has invalid characters",
uid: "f8cc010c.ee72.4681;89d2+d46e1bd47d33",
expected: "f8cc010c-ee72-4681-89d2-d46e1bd47d33",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, AutofixUID(tt.uid))
})
}
}

View File

@ -21,6 +21,7 @@ import { ScopesScene } from './ScopesScene';
import { ScopesTreeLevel } from './ScopesTreeLevel';
import { fetchNodes, fetchScope, fetchSelectedScopes } from './api';
import { NodesMap, SelectedScope, TreeScope } from './types';
import { getBasicScope } from './utils';
export interface ScopesFiltersSceneState extends SceneObjectState {
nodes: NodesMap;
@ -180,9 +181,16 @@ export class ScopesFiltersScene extends SceneObjectBase<ScopesFiltersSceneState>
return;
}
this.setState({ treeScopes, isLoadingScopes: true });
this.setState({
// Update the scopes with the basic scopes otherwise they'd be lost between URL syncs
scopes: treeScopes.map(({ scopeName, path }) => ({ scope: getBasicScope(scopeName), path })),
treeScopes,
isLoadingScopes: true,
});
this.setState({ scopes: await fetchSelectedScopes(treeScopes), isLoadingScopes: false });
const scopes = await fetchSelectedScopes(treeScopes);
this.setState({ scopes, isLoadingScopes: false });
}
public resetDirtyScopeNames() {

View File

@ -30,7 +30,7 @@ export class ScopesScene extends SceneObjectBase<ScopesSceneState> {
this.addActivationHandler(() => {
this._subs.add(
this.state.filters.subscribeToState((newState, prevState) => {
if (newState.scopes !== prevState.scopes) {
if (!newState.isLoadingScopes && newState.scopes !== prevState.scopes) {
if (this.state.isExpanded) {
this.state.dashboards.fetchDashboards(this.state.filters.getSelectedScopes());
}

View File

@ -3,6 +3,7 @@ import { config, getBackendSrv } from '@grafana/runtime';
import { ScopedResourceClient } from 'app/features/apiserver/client';
import { NodesMap, SelectedScope, SuggestedDashboard, TreeScope } from './types';
import { getBasicScope, mergeScopes } from './utils';
const group = 'scope.grafana.app';
const version = 'v0alpha1';
@ -48,31 +49,12 @@ export async function fetchScope(name: string): Promise<Scope> {
}
const response = new Promise<Scope>(async (resolve) => {
const basicScope: Scope = {
metadata: { name },
spec: {
filters: [],
title: name,
type: '',
category: '',
description: '',
},
};
const basicScope = getBasicScope(name);
try {
const serverScope = await scopesClient.get(name);
const scope = {
...basicScope,
metadata: {
...basicScope.metadata,
...serverScope.metadata,
},
spec: {
...basicScope.spec,
...serverScope.spec,
},
};
const scope = mergeScopes(basicScope, serverScope);
resolve(scope);
} catch (err) {

View File

@ -0,0 +1,28 @@
import { Scope } from '@grafana/data';
export function getBasicScope(name: string): Scope {
return {
metadata: { name },
spec: {
filters: [],
title: name,
type: '',
category: '',
description: '',
},
};
}
export function mergeScopes(scope1: Scope, scope2: Scope): Scope {
return {
...scope1,
metadata: {
...scope1.metadata,
...scope2.metadata,
},
spec: {
...scope1.spec,
...scope2.spec,
},
};
}

View File

@ -16,7 +16,10 @@ export interface ContentOutlineContextProps {
outlineItems: ContentOutlineItemContextProps[];
register: RegisterFunction;
unregister: (id: string) => void;
unregisterAllChildren: (parentId: string, childType: ITEM_TYPES) => void;
unregisterAllChildren: (
parentIdGetter: (items: ContentOutlineItemContextProps[]) => string | undefined,
childType: ITEM_TYPES
) => void;
updateOutlineItems: (newItems: ContentOutlineItemContextProps[]) => void;
updateItem: (id: string, properties: Partial<Omit<ContentOutlineItemContextProps, 'id'>>) => void;
}
@ -193,16 +196,23 @@ export function ContentOutlineContextProvider({ children, refreshDependencies }:
);
}, []);
const unregisterAllChildren = useCallback((parentId: string, childType: ITEM_TYPES) => {
setOutlineItems((prevItems) =>
prevItems.map((item) => {
if (item.id === parentId) {
item.children = item.children?.filter((child) => child.type !== childType);
const unregisterAllChildren = useCallback(
(parentIdGetter: (items: ContentOutlineItemContextProps[]) => string | undefined, childType: ITEM_TYPES) => {
setOutlineItems((prevItems) => {
const parentId = parentIdGetter(prevItems);
if (!parentId) {
return prevItems;
}
return item;
})
);
}, []);
return prevItems.map((item) => {
if (item.id === parentId) {
item.children = item.children?.filter((child) => child.type !== childType);
}
return item;
});
});
},
[]
);
useEffect(() => {
setOutlineItems((prevItems) => {

View File

@ -1,5 +1,5 @@
import { identity } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { usePrevious } from 'react-use';
import {
@ -154,8 +154,10 @@ export function ExploreGraph({
const structureRev = useStructureRev(dataWithConfig);
const onHiddenSeriesChangedRef = useRef(onHiddenSeriesChanged);
useEffect(() => {
if (onHiddenSeriesChanged) {
if (onHiddenSeriesChangedRef.current) {
const hiddenFrames: string[] = [];
dataWithConfig.forEach((frame) => {
const allFieldsHidden = frame.fields.map((field) => field.config?.custom?.hideFrom?.viz).every(identity);
@ -163,9 +165,9 @@ export function ExploreGraph({
hiddenFrames.push(getFrameDisplayName(frame));
}
});
onHiddenSeriesChanged(hiddenFrames);
onHiddenSeriesChangedRef.current(hiddenFrames);
}
}, [dataWithConfig, onHiddenSeriesChanged]);
}, [dataWithConfig]);
const panelContext: PanelContext = {
eventsScope: 'explore',

View File

@ -1,11 +1,11 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React, { ComponentProps } from 'react';
import { Provider } from 'react-redux';
import {
DataFrame,
EventBusSrv,
ExploreLogsPanelState,
ExplorePanelsState,
LoadingState,
LogLevel,
@ -13,25 +13,20 @@ import {
standardTransformersRegistry,
toUtc,
createDataFrame,
ExploreLogsPanelState,
} from '@grafana/data';
import { organizeFieldsTransformer } from '@grafana/data/src/transformations/transformers/organize';
import { config } from '@grafana/runtime';
import store from 'app/core/store';
import { extractFieldsTransformer } from 'app/features/transformers/extractFields/extractFields';
import { configureStore } from 'app/store/configureStore';
import { initialExploreState } from '../state/main';
import { makeExplorePaneState } from '../state/utils';
import { Logs } from './Logs';
import { visualisationTypeKey } from './utils/logs';
import { getMockElasticFrame, getMockLokiFrame } from './utils/testMocks.test';
jest.mock('app/core/store', () => {
return {
getBool: jest.fn(),
getObject: jest.fn((_a, b) => b),
get: jest.fn(),
set: jest.fn(),
};
});
const reportInteraction = jest.fn();
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
@ -45,26 +40,11 @@ jest.mock('app/core/utils/shortLinks', () => ({
createAndCopyShortLink: (url: string) => createAndCopyShortLink(url),
}));
jest.mock('app/store/store', () => ({
getState: jest.fn().mockReturnValue({
explore: {
panes: {
left: {
datasource: 'id',
queries: [{ refId: 'A', expr: '', queryType: 'range', datasource: { type: 'loki', uid: 'id' } }],
range: { raw: { from: 'now-1h', to: 'now' } },
},
},
},
}),
dispatch: jest.fn(),
}));
const changePanelState = jest.fn();
const fakeChangePanelState = jest.fn().mockReturnValue({ type: 'fakeAction' });
jest.mock('../state/explorePane', () => ({
...jest.requireActual('../state/explorePane'),
changePanelState: (exploreId: string, panel: 'logs', panelState: {} | ExploreLogsPanelState) => {
return changePanelState(exploreId, panel, panelState);
return fakeChangePanelState(exploreId, panel, panelState);
},
}));
@ -72,6 +52,7 @@ describe('Logs', () => {
let originalHref = window.location.href;
beforeEach(() => {
localStorage.clear();
jest.clearAllMocks();
});
@ -120,6 +101,7 @@ describe('Logs', () => {
];
const testDataFrame = dataFrame ?? getMockLokiFrame();
return (
<Logs
exploreId={'left'}
@ -157,8 +139,23 @@ describe('Logs', () => {
/>
);
};
const setup = (partialProps?: Partial<ComponentProps<typeof Logs>>, dataFrame?: DataFrame, logs?: LogRowModel[]) => {
return render(getComponent(partialProps, dataFrame ? dataFrame : getMockLokiFrame(), logs));
const fakeStore = configureStore({
explore: {
...initialExploreState,
panes: {
left: makeExplorePaneState(),
},
},
});
const { rerender } = render(
<Provider store={fakeStore}>
{getComponent(partialProps, dataFrame ? dataFrame : getMockLokiFrame(), logs)}
</Provider>
);
return { rerender, store: fakeStore };
};
describe('scrolling behavior', () => {
@ -216,40 +213,47 @@ describe('Logs', () => {
it('should render a load more button', () => {
const scanningStarted = jest.fn();
const store = configureStore({
explore: {
...initialExploreState,
},
});
render(
<Logs
exploreId={'left'}
splitOpen={() => undefined}
logsVolumeEnabled={true}
onSetLogsVolumeEnabled={() => null}
onClickFilterLabel={() => null}
onClickFilterOutLabel={() => null}
logsVolumeData={undefined}
loadLogsVolumeData={() => undefined}
logRows={[]}
onStartScanning={scanningStarted}
timeZone={'utc'}
width={50}
loading={false}
loadingState={LoadingState.Done}
absoluteRange={{
from: toUtc('2019-01-01 10:00:00').valueOf(),
to: toUtc('2019-01-01 16:00:00').valueOf(),
}}
range={{
from: toUtc('2019-01-01 10:00:00'),
to: toUtc('2019-01-01 16:00:00'),
raw: { from: 'now-1h', to: 'now' },
}}
addResultsToCache={() => {}}
onChangeTime={() => {}}
clearCache={() => {}}
getFieldLinks={() => {
return [];
}}
eventBus={new EventBusSrv()}
isFilterLabelActive={jest.fn()}
/>
<Provider store={store}>
<Logs
exploreId={'left'}
splitOpen={() => undefined}
logsVolumeEnabled={true}
onSetLogsVolumeEnabled={() => null}
onClickFilterLabel={() => null}
onClickFilterOutLabel={() => null}
logsVolumeData={undefined}
loadLogsVolumeData={() => undefined}
logRows={[]}
onStartScanning={scanningStarted}
timeZone={'utc'}
width={50}
loading={false}
loadingState={LoadingState.Done}
absoluteRange={{
from: toUtc('2019-01-01 10:00:00').valueOf(),
to: toUtc('2019-01-01 16:00:00').valueOf(),
}}
range={{
from: toUtc('2019-01-01 10:00:00'),
to: toUtc('2019-01-01 16:00:00'),
raw: { from: 'now-1h', to: 'now' },
}}
addResultsToCache={() => {}}
onChangeTime={() => {}}
clearCache={() => {}}
getFieldLinks={() => {
return [];
}}
eventBus={new EventBusSrv()}
isFilterLabelActive={jest.fn()}
/>
</Provider>
);
const button = screen.getByRole('button', {
name: /scan for older logs/i,
@ -259,40 +263,47 @@ describe('Logs', () => {
});
it('should render a stop scanning button', () => {
const store = configureStore({
explore: {
...initialExploreState,
},
});
render(
<Logs
exploreId={'left'}
splitOpen={() => undefined}
logsVolumeEnabled={true}
onSetLogsVolumeEnabled={() => null}
onClickFilterLabel={() => null}
onClickFilterOutLabel={() => null}
logsVolumeData={undefined}
loadLogsVolumeData={() => undefined}
logRows={[]}
scanning={true}
timeZone={'utc'}
width={50}
loading={false}
loadingState={LoadingState.Done}
absoluteRange={{
from: toUtc('2019-01-01 10:00:00').valueOf(),
to: toUtc('2019-01-01 16:00:00').valueOf(),
}}
range={{
from: toUtc('2019-01-01 10:00:00'),
to: toUtc('2019-01-01 16:00:00'),
raw: { from: 'now-1h', to: 'now' },
}}
addResultsToCache={() => {}}
onChangeTime={() => {}}
clearCache={() => {}}
getFieldLinks={() => {
return [];
}}
eventBus={new EventBusSrv()}
isFilterLabelActive={jest.fn()}
/>
<Provider store={store}>
<Logs
exploreId={'left'}
splitOpen={() => undefined}
logsVolumeEnabled={true}
onSetLogsVolumeEnabled={() => null}
onClickFilterLabel={() => null}
onClickFilterOutLabel={() => null}
logsVolumeData={undefined}
loadLogsVolumeData={() => undefined}
logRows={[]}
scanning={true}
timeZone={'utc'}
width={50}
loading={false}
loadingState={LoadingState.Done}
absoluteRange={{
from: toUtc('2019-01-01 10:00:00').valueOf(),
to: toUtc('2019-01-01 16:00:00').valueOf(),
}}
range={{
from: toUtc('2019-01-01 10:00:00'),
to: toUtc('2019-01-01 16:00:00'),
raw: { from: 'now-1h', to: 'now' },
}}
addResultsToCache={() => {}}
onChangeTime={() => {}}
clearCache={() => {}}
getFieldLinks={() => {
return [];
}}
eventBus={new EventBusSrv()}
isFilterLabelActive={jest.fn()}
/>
</Provider>
);
expect(
@ -304,42 +315,48 @@ describe('Logs', () => {
it('should render a stop scanning button', () => {
const scanningStopped = jest.fn();
const store = configureStore({
explore: {
...initialExploreState,
},
});
render(
<Logs
exploreId={'left'}
splitOpen={() => undefined}
logsVolumeEnabled={true}
onSetLogsVolumeEnabled={() => null}
onClickFilterLabel={() => null}
onClickFilterOutLabel={() => null}
logsVolumeData={undefined}
loadLogsVolumeData={() => undefined}
logRows={[]}
scanning={true}
onStopScanning={scanningStopped}
timeZone={'utc'}
width={50}
loading={false}
loadingState={LoadingState.Done}
absoluteRange={{
from: toUtc('2019-01-01 10:00:00').valueOf(),
to: toUtc('2019-01-01 16:00:00').valueOf(),
}}
range={{
from: toUtc('2019-01-01 10:00:00'),
to: toUtc('2019-01-01 16:00:00'),
raw: { from: 'now-1h', to: 'now' },
}}
addResultsToCache={() => {}}
onChangeTime={() => {}}
clearCache={() => {}}
getFieldLinks={() => {
return [];
}}
eventBus={new EventBusSrv()}
isFilterLabelActive={jest.fn()}
/>
<Provider store={store}>
<Logs
exploreId={'left'}
splitOpen={() => undefined}
logsVolumeEnabled={true}
onSetLogsVolumeEnabled={() => null}
onClickFilterLabel={() => null}
onClickFilterOutLabel={() => null}
logsVolumeData={undefined}
loadLogsVolumeData={() => undefined}
logRows={[]}
scanning={true}
onStopScanning={scanningStopped}
timeZone={'utc'}
width={50}
loading={false}
loadingState={LoadingState.Done}
absoluteRange={{
from: toUtc('2019-01-01 10:00:00').valueOf(),
to: toUtc('2019-01-01 16:00:00').valueOf(),
}}
range={{
from: toUtc('2019-01-01 10:00:00'),
to: toUtc('2019-01-01 16:00:00'),
raw: { from: 'now-1h', to: 'now' },
}}
addResultsToCache={() => {}}
onChangeTime={() => {}}
clearCache={() => {}}
getFieldLinks={() => {
return [];
}}
eventBus={new EventBusSrv()}
isFilterLabelActive={jest.fn()}
/>
</Provider>
);
const button = screen.getByRole('button', {
@ -363,12 +380,12 @@ describe('Logs', () => {
describe('for permalinking', () => {
it('should dispatch a `changePanelState` event without the id', () => {
const panelState = { logs: { id: '1' } };
const { rerender } = setup({ loading: false, panelState });
const { rerender, store } = setup({ loading: false, panelState });
rerender(getComponent({ loading: true, exploreId: 'right', panelState }));
rerender(getComponent({ loading: false, exploreId: 'right', panelState }));
rerender(<Provider store={store}>{getComponent({ loading: true, exploreId: 'right', panelState })}</Provider>);
rerender(<Provider store={store}>{getComponent({ loading: false, exploreId: 'right', panelState })}</Provider>);
expect(changePanelState).toHaveBeenCalledWith('right', 'logs', { logs: {} });
expect(fakeChangePanelState).toHaveBeenCalledWith('right', 'logs', { logs: {} });
});
it('should scroll the scrollElement into view if rows contain id', () => {
@ -491,23 +508,17 @@ describe('Logs', () => {
});
it('should use default state from localstorage - table', async () => {
const oldGet = store.get;
store.get = jest.fn().mockReturnValue('table');
localStorage.setItem(visualisationTypeKey, 'table');
setup({});
const table = await screen.findByTestId('logRowsTable');
expect(table).toBeInTheDocument();
store.get = oldGet;
});
it('should use default state from localstorage - logs', async () => {
const oldGet = store.get;
store.get = jest.fn().mockReturnValue('logs');
localStorage.setItem(visualisationTypeKey, 'logs');
setup({});
const table = await screen.findByTestId('logRows');
expect(table).toBeInTheDocument();
store.get = oldGet;
});
it('should change visualisation to table on toggle (elastic)', async () => {

File diff suppressed because it is too large Load Diff

View File

@ -270,7 +270,7 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
};
}
if (initializeExplore.pending.match(action)) {
if (initializeExplore?.pending.match(action)) {
const initialPanes = Object.entries(state.panes);
const before = initialPanes.slice(0, action.meta.arg.position);
const after = initialPanes.slice(before.length);

View File

@ -71,10 +71,7 @@ export const GroupByField = (props: Props) => {
const scopeOptions = Object.values(TraceqlSearchScope).map((t) => ({ label: t, value: t }));
return (
<InlineSearchField
label="Aggregate by"
tooltip="Select one or more tags to see the metrics summary. Note: the metrics summary API only considers spans of kind = server."
>
<InlineSearchField label="Aggregate by" tooltip="Select one or more tags to see the metrics summary.">
<>
{query.groupBy?.map((f, i) => {
const tags = getTags(f)

View File

@ -536,9 +536,7 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
if (!response.data.summaries) {
return {
error: {
message: getErrorMessage(
`No summary data for '${groupBy}'. Note: the metrics summary API only considers spans of kind = server. You can check if the attributes exist by running a TraceQL query like { attr_key = attr_value && kind = server }`
),
message: getErrorMessage(`No summary data for '${groupBy}'.`),
},
data: emptyResponse,
};

View File

@ -58,7 +58,7 @@ describe('MetricsSummary', () => {
"datasourceName": "tempo",
"datasourceUid": "gdev-tempo",
"query": {
"query": "{name="HTTP POST - post" && span.http.status_code=\${__data.fields["span.http.status_code"]} && temperature=\${__data.fields["temperature"]} && kind=server} | by(resource.service.name)",
"query": "{name="HTTP POST - post" && span.http.status_code=\${__data.fields["span.http.status_code"]} && temperature=\${__data.fields["temperature"]}} | by(resource.service.name)",
"queryType": "traceql",
},
},
@ -83,7 +83,7 @@ describe('MetricsSummary', () => {
"datasourceName": "tempo",
"datasourceUid": "gdev-tempo",
"query": {
"query": "{name="HTTP POST - post" && span.http.status_code=\${__data.fields["span.http.status_code"]} && temperature=\${__data.fields["temperature"]} && kind=server} | by(resource.service.name)",
"query": "{name="HTTP POST - post" && span.http.status_code=\${__data.fields["span.http.status_code"]} && temperature=\${__data.fields["temperature"]}} | by(resource.service.name)",
"queryType": "traceql",
},
},
@ -99,19 +99,6 @@ describe('MetricsSummary', () => {
38.1,
],
},
{
"config": {
"custom": {
"width": 150,
},
"displayNameFromDS": "Kind",
},
"name": "kind",
"type": "string",
"values": [
"server",
],
},
{
"config": {
"custom": {
@ -222,7 +209,6 @@ describe('MetricsSummary', () => {
{
"contains_sink": "true",
"errorPercentage": 10,
"kind": "server",
"p50": 1,
"p90": 2,
"p95": 3,
@ -241,21 +227,21 @@ describe('MetricsSummary', () => {
it('getConfigQuery should return correctly for empty target query', () => {
const result = getConfigQuery(series, '{}');
expect(result).toEqual(
'{span.http.status_code=${__data.fields["span.http.status_code"]} && temperature=${__data.fields["temperature"]} && room="${__data.fields["room"]}" && contains_sink="${__data.fields["contains_sink"]}" && window_open="${__data.fields["window_open"]}" && spanStatus=${__data.fields["spanStatus"]} && spanKind=${__data.fields["spanKind"]} && kind=server}'
'{span.http.status_code=${__data.fields["span.http.status_code"]} && temperature=${__data.fields["temperature"]} && room="${__data.fields["room"]}" && contains_sink="${__data.fields["contains_sink"]}" && window_open="${__data.fields["window_open"]}" && spanStatus=${__data.fields["spanStatus"]} && spanKind=${__data.fields["spanKind"]}}'
);
});
it('getConfigQuery should return correctly for target query', () => {
const result = getConfigQuery(series, '{name="HTTP POST - post"} | by(resource.service.name)');
expect(result).toEqual(
'{name="HTTP POST - post" && span.http.status_code=${__data.fields["span.http.status_code"]} && temperature=${__data.fields["temperature"]} && room="${__data.fields["room"]}" && contains_sink="${__data.fields["contains_sink"]}" && window_open="${__data.fields["window_open"]}" && spanStatus=${__data.fields["spanStatus"]} && spanKind=${__data.fields["spanKind"]} && kind=server} | by(resource.service.name)'
'{name="HTTP POST - post" && span.http.status_code=${__data.fields["span.http.status_code"]} && temperature=${__data.fields["temperature"]} && room="${__data.fields["room"]}" && contains_sink="${__data.fields["contains_sink"]}" && window_open="${__data.fields["window_open"]}" && spanStatus=${__data.fields["spanStatus"]} && spanKind=${__data.fields["spanKind"]}} | by(resource.service.name)'
);
});
it('getConfigQuery should return correctly for target query without brackets', () => {
const result = getConfigQuery(series, 'by(resource.service.name)');
expect(result).toEqual(
'{span.http.status_code=${__data.fields["span.http.status_code"]} && temperature=${__data.fields["temperature"]} && room="${__data.fields["room"]}" && contains_sink="${__data.fields["contains_sink"]}" && window_open="${__data.fields["window_open"]}" && spanStatus=${__data.fields["spanStatus"]} && spanKind=${__data.fields["spanKind"]} && kind=server} | by(resource.service.name)'
'{span.http.status_code=${__data.fields["span.http.status_code"]} && temperature=${__data.fields["temperature"]} && room="${__data.fields["room"]}" && contains_sink="${__data.fields["contains_sink"]}" && window_open="${__data.fields["window_open"]}" && spanStatus=${__data.fields["spanStatus"]} && spanKind=${__data.fields["spanKind"]}} | by(resource.service.name)'
);
});
});

View File

@ -70,11 +70,6 @@ export function createTableFrameFromMetricsSummaryQuery(
refId: 'metrics-summary',
fields: [
...Object.values(dynamicMetrics).sort((a, b) => a.name.localeCompare(b.name)),
{
name: 'kind',
type: FieldType.string,
config: { displayNameFromDS: 'Kind', custom: { width: 150 } },
},
{
name: 'spanCount',
type: FieldType.number,
@ -113,7 +108,6 @@ export const transformToMetricsData = (data: MetricsSummary) => {
: '0%';
const metricsData: MetricsData = {
kind: 'server', // so the user knows all results are of kind = server
spanCount: getNumberForMetric(data.spanCount),
errorPercentage,
p50: getNumberForMetric(data.p50),
@ -145,12 +139,12 @@ export const getConfigQuery = (series: Series[], targetQuery: string) => {
configQuery = targetQuery.substring(0, closingBracketIndex);
if (queryParts.length > 0) {
configQuery += targetQuery.replace(/\s/g, '').includes('{}') ? '' : ' && ';
configQuery += `${queryParts.join(' && ')} && kind=server`;
configQuery += `${queryParts.join(' && ')}`;
configQuery += `}`;
}
configQuery += `${queryAfterClosingBracket}`;
} else {
configQuery = `{${queryParts.join(' && ')} && kind=server} | ${targetQuery}`;
configQuery = `{${queryParts.join(' && ')}} | ${targetQuery}`;
}
return configQuery;

View File

@ -25,6 +25,14 @@
"user": "Nutzer"
}
},
"alert-labels": {
"button": {
"hide": "",
"show": {
"tooltip": ""
}
}
},
"alert-rule-form": {
"evaluation-behaviour": {
"description": {
@ -138,6 +146,16 @@
"text": ""
}
},
"central-alert-history": {
"error": "",
"filter": {
"button": {
"clear": ""
},
"label": "",
"placeholder": ""
}
},
"clipboard-button": {
"inline-toast": {
"success": "Kopiert"
@ -1166,7 +1184,7 @@
"public": {
"title": "Öffentliche Dashboards"
},
"recentlyDeleted": {
"recently-deleted": {
"subtitle": "",
"title": ""
},
@ -1615,6 +1633,7 @@
"tree": {
"collapse": "",
"expand": "",
"headline": "",
"search": ""
}
},

View File

@ -25,6 +25,14 @@
"user": "Usuario"
}
},
"alert-labels": {
"button": {
"hide": "",
"show": {
"tooltip": ""
}
}
},
"alert-rule-form": {
"evaluation-behaviour": {
"description": {
@ -138,6 +146,16 @@
"text": ""
}
},
"central-alert-history": {
"error": "",
"filter": {
"button": {
"clear": ""
},
"label": "",
"placeholder": ""
}
},
"clipboard-button": {
"inline-toast": {
"success": "Copiado"
@ -1166,7 +1184,7 @@
"public": {
"title": "Paneles de control públicos"
},
"recentlyDeleted": {
"recently-deleted": {
"subtitle": "",
"title": ""
},
@ -1615,6 +1633,7 @@
"tree": {
"collapse": "",
"expand": "",
"headline": "",
"search": ""
}
},

View File

@ -25,6 +25,14 @@
"user": "Utilisateur"
}
},
"alert-labels": {
"button": {
"hide": "",
"show": {
"tooltip": ""
}
}
},
"alert-rule-form": {
"evaluation-behaviour": {
"description": {
@ -138,6 +146,16 @@
"text": ""
}
},
"central-alert-history": {
"error": "",
"filter": {
"button": {
"clear": ""
},
"label": "",
"placeholder": ""
}
},
"clipboard-button": {
"inline-toast": {
"success": "Copié"
@ -1166,7 +1184,7 @@
"public": {
"title": "Tableaux de bord publics"
},
"recentlyDeleted": {
"recently-deleted": {
"subtitle": "",
"title": ""
},
@ -1615,6 +1633,7 @@
"tree": {
"collapse": "",
"expand": "",
"headline": "",
"search": ""
}
},

View File

@ -25,6 +25,14 @@
"user": "Usuário"
}
},
"alert-labels": {
"button": {
"hide": "",
"show": {
"tooltip": ""
}
}
},
"alert-rule-form": {
"evaluation-behaviour": {
"description": {
@ -138,6 +146,16 @@
"text": ""
}
},
"central-alert-history": {
"error": "",
"filter": {
"button": {
"clear": ""
},
"label": "",
"placeholder": ""
}
},
"clipboard-button": {
"inline-toast": {
"success": "Copiado"
@ -1166,7 +1184,7 @@
"public": {
"title": "Painéis de controle públicos"
},
"recentlyDeleted": {
"recently-deleted": {
"subtitle": "",
"title": ""
},
@ -1615,6 +1633,7 @@
"tree": {
"collapse": "",
"expand": "",
"headline": "",
"search": ""
}
},

View File

@ -25,6 +25,14 @@
"user": "用户"
}
},
"alert-labels": {
"button": {
"hide": "",
"show": {
"tooltip": ""
}
}
},
"alert-rule-form": {
"evaluation-behaviour": {
"description": {
@ -133,6 +141,16 @@
"text": ""
}
},
"central-alert-history": {
"error": "",
"filter": {
"button": {
"clear": ""
},
"label": "",
"placeholder": ""
}
},
"clipboard-button": {
"inline-toast": {
"success": "已复制"
@ -1160,7 +1178,7 @@
"public": {
"title": "公共仪表板"
},
"recentlyDeleted": {
"recently-deleted": {
"subtitle": "",
"title": ""
},
@ -1608,6 +1626,7 @@
"tree": {
"collapse": "",
"expand": "",
"headline": "",
"search": ""
}
},

View File

@ -3555,8 +3555,8 @@ __metadata:
linkType: soft
"@grafana/scenes@npm:^5.0.2":
version: 5.0.3
resolution: "@grafana/scenes@npm:5.0.3"
version: 5.1.2
resolution: "@grafana/scenes@npm:5.1.2"
dependencies:
"@grafana/e2e-selectors": "npm:^11.0.0"
"@leeoniya/ufuzzy": "npm:^1.0.14"
@ -3571,7 +3571,7 @@ __metadata:
"@grafana/ui": ^10.4.1
react: ^18.0.0
react-dom: ^18.0.0
checksum: 10/d99e88ba26f6df34fa595656be20cdaee8e9e59a8928e80691bea5cd9646ef6534c9929f38a5fcca1b09891a2609d4ee8e96f898799c5ec73b1993abe09b094a
checksum: 10/814fe81537d267640cf0e4d91c1fc5805290fd6f46bbf37633edfbc8fbeae8a064a2b9540d482adb061235bd20148ddf246db6e41a8141380e98d49ca31638f3
languageName: node
linkType: hard
@ -3684,6 +3684,7 @@ __metadata:
"@storybook/react": "npm:^8.1.6"
"@storybook/react-webpack5": "npm:^8.1.6"
"@storybook/theming": "npm:^8.1.6"
"@tanstack/react-virtual": "npm:^3.5.1"
"@testing-library/dom": "npm:10.0.0"
"@testing-library/jest-dom": "npm:6.4.2"
"@testing-library/react": "npm:15.0.2"
@ -7803,6 +7804,25 @@ __metadata:
languageName: node
linkType: hard
"@tanstack/react-virtual@npm:^3.5.1":
version: 3.5.1
resolution: "@tanstack/react-virtual@npm:3.5.1"
dependencies:
"@tanstack/virtual-core": "npm:3.5.1"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 10/11c8e9e2391fa0c947848a720b7dccccb1e35a78ac3169d1c34629bbec4ec713eed78d4c17a3e540e01386ee25b600a53254357597ae91a5fe35c7436651e975
languageName: node
linkType: hard
"@tanstack/virtual-core@npm:3.5.1":
version: 3.5.1
resolution: "@tanstack/virtual-core@npm:3.5.1"
checksum: 10/611ea09d37cf9183a51d2dfce401c3802b0d91f014e9bbaf32a6220ec7301b873b308130b795d935c0f5b73a43fd8358274915885da692d3e991eeeab6f8711b
languageName: node
linkType: hard
"@testing-library/dom@npm:10.0.0, @testing-library/dom@npm:>=7, @testing-library/dom@npm:^10.0.0":
version: 10.0.0
resolution: "@testing-library/dom@npm:10.0.0"
@ -14704,7 +14724,7 @@ __metadata:
languageName: node
linkType: hard
"entities@npm:^4.2.0, entities@npm:^4.3.0, entities@npm:^4.4.0":
"entities@npm:^4.2.0, entities@npm:^4.4.0":
version: 4.4.0
resolution: "entities@npm:4.4.0"
checksum: 10/b627cb900e901cc7817037b83bf993a1cbf6a64850540f7526af7bcf9c7d09ebc671198e6182cfae4680f733799e2852e6a1c46aa62ff36eb99680057a038df5
@ -17883,19 +17903,7 @@ __metadata:
languageName: node
linkType: hard
"htmlparser2@npm:^8.0.1":
version: 8.0.1
resolution: "htmlparser2@npm:8.0.1"
dependencies:
domelementtype: "npm:^2.3.0"
domhandler: "npm:^5.0.2"
domutils: "npm:^3.0.1"
entities: "npm:^4.3.0"
checksum: 10/f891041c331ef7ef300f1e8f0e6756d663cf8096f8a343a1bf474e7a5ce34fe7cd71b9dfb0227277f7de2007e847ef2a447e8b48eab592d6f3631aae18301d22
languageName: node
linkType: hard
"htmlparser2@npm:^8.0.2":
"htmlparser2@npm:^8.0.1, htmlparser2@npm:^8.0.2":
version: 8.0.2
resolution: "htmlparser2@npm:8.0.2"
dependencies:
@ -24769,7 +24777,7 @@ __metadata:
languageName: node
linkType: hard
"punycode@npm:2.3.1":
"punycode@npm:2.3.1, punycode@npm:^2.1.0, punycode@npm:^2.1.1":
version: 2.3.1
resolution: "punycode@npm:2.3.1"
checksum: 10/febdc4362bead22f9e2608ff0171713230b57aff9dddc1c273aa2a651fbd366f94b7d6a71d78342a7c0819906750351ca7f2edd26ea41b626d87d6a13d1bd059
@ -24783,13 +24791,6 @@ __metadata:
languageName: node
linkType: hard
"punycode@npm:^2.1.0, punycode@npm:^2.1.1":
version: 2.1.1
resolution: "punycode@npm:2.1.1"
checksum: 10/939daa010c2cacebdb060c40ecb52fef0a739324a66f7fffe0f94353a1ee83e3b455e9032054c4a0c4977b0a28e27086f2171c392832b59a01bd948fd8e20914
languageName: node
linkType: hard
"pure-rand@npm:^6.0.0":
version: 6.0.3
resolution: "pure-rand@npm:6.0.3"