Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13a921a9fa | |||
| 039bec7c18 | |||
| 91525c05c8 |
@@ -543,7 +543,6 @@ i18next.config.ts @grafana/grafana-frontend-platform
|
||||
/packages/grafana-data/tsconfig.json @grafana/grafana-frontend-platform
|
||||
/packages/grafana-data/test/ @grafana/grafana-frontend-platform
|
||||
/packages/grafana-data/typings/ @grafana/grafana-frontend-platform
|
||||
/packages/grafana-data/scripts/ @grafana/grafana-frontend-platform
|
||||
|
||||
/packages/grafana-data/src/**/*logs* @grafana/observability-logs
|
||||
/packages/grafana-data/src/context/plugins/ @grafana/plugins-platform-frontend
|
||||
|
||||
@@ -121,8 +121,6 @@ linters:
|
||||
- '**/pkg/tsdb/zipkin/**/*'
|
||||
- '**/pkg/tsdb/jaeger/*'
|
||||
- '**/pkg/tsdb/jaeger/**/*'
|
||||
- '**/pkg/tsdb/elasticsearch/*'
|
||||
- '**/pkg/tsdb/elasticsearch/**/*'
|
||||
deny:
|
||||
- pkg: github.com/grafana/grafana/pkg/api
|
||||
desc: Core plugins are not allowed to depend on Grafana core packages
|
||||
|
||||
@@ -103,11 +103,10 @@ To configure basic settings for the data source, complete the following steps:
|
||||
|
||||
1. Set the data source's basic configuration options:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | Sets the name you use to refer to the data source in panels and queries. |
|
||||
| **Default** | Sets whether the data source is pre-selected for new panels. |
|
||||
| **Universe Domain** | The universe domain to connect to. For more information, refer to [Documentation on universe domains](https://docs.cloud.google.com/python/docs/reference/monitoring/latest/google.cloud.monitoring_v3.services.service_monitoring_service.ServiceMonitoringServiceAsyncClient#google_cloud_monitoring_v3_services_service_monitoring_service_ServiceMonitoringServiceAsyncClient_universe_domain). Defaults to `googleapis.com`. |
|
||||
| Name | Description |
|
||||
| ----------- | ------------------------------------------------------------------------ |
|
||||
| **Name** | Sets the name you use to refer to the data source in panels and queries. |
|
||||
| **Default** | Sets whether the data source is pre-selected for new panels. |
|
||||
|
||||
### Provision the data source
|
||||
|
||||
@@ -130,7 +129,6 @@ datasources:
|
||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||
authenticationType: jwt
|
||||
defaultProject: my-project-name
|
||||
universeDomain: googleapis.com
|
||||
secureJsonData:
|
||||
privateKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
@@ -154,7 +152,6 @@ datasources:
|
||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||
authenticationType: jwt
|
||||
defaultProject: my-project-name
|
||||
universeDomain: googleapis.com
|
||||
privateKeyPath: /etc/secrets/gce.pem
|
||||
```
|
||||
|
||||
@@ -169,7 +166,6 @@ datasources:
|
||||
access: proxy
|
||||
jsonData:
|
||||
authenticationType: gce
|
||||
universeDomain: googleapis.com
|
||||
```
|
||||
|
||||
## Import pre-configured dashboards
|
||||
|
||||
@@ -87,7 +87,6 @@ With a Grafana Enterprise license, you also get access to premium data sources,
|
||||
- [CockroachDB](/grafana/plugins/grafana-cockroachdb-datasource)
|
||||
- [Databricks](/grafana/plugins/grafana-databricks-datasource)
|
||||
- [DataDog](/grafana/plugins/grafana-datadog-datasource)
|
||||
- [IBM Db2](/grafana/plugins/grafana-ibmdb2-datasource)
|
||||
- [Drone](/grafana/plugins/grafana-drone-datasource)
|
||||
- [DynamoDB](/grafana/plugins/grafana-dynamodb-datasource/)
|
||||
- [Dynatrace](/grafana/plugins/grafana-dynatrace-datasource)
|
||||
|
||||
@@ -3743,21 +3743,46 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/aggregations.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/aggregations.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/configuration/DataLinks.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
|
||||
@@ -44,8 +44,8 @@ require (
|
||||
github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group
|
||||
github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect; @grafana/grafana-developer-enablement-squad
|
||||
github.com/blevesearch/bleve/v2 v2.5.7 // @grafana/grafana-search-and-storage
|
||||
github.com/blevesearch/bleve_index_api v1.3.0 // @grafana/grafana-search-and-storage
|
||||
github.com/blevesearch/bleve/v2 v2.5.0 // @grafana/grafana-search-and-storage
|
||||
github.com/blevesearch/bleve_index_api v1.2.7 // @grafana/grafana-search-and-storage
|
||||
github.com/blugelabs/bluge v0.2.2 // @grafana/grafana-backend-group
|
||||
github.com/blugelabs/bluge_segment_api v0.2.0 // @grafana/grafana-backend-group
|
||||
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // @grafana/grafana-backend-group
|
||||
@@ -365,22 +365,22 @@ require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.22.0 // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/blevesearch/geo v0.2.4 // indirect
|
||||
github.com/blevesearch/go-faiss v1.0.26 // indirect
|
||||
github.com/blevesearch/geo v0.1.20 // indirect
|
||||
github.com/blevesearch/go-faiss v1.0.25 // indirect
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
||||
github.com/blevesearch/gtreap v0.1.1 // indirect
|
||||
github.com/blevesearch/mmap-go v1.0.4 // indirect
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 // indirect
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.9 // indirect
|
||||
github.com/blevesearch/segment v0.9.1 // indirect
|
||||
github.com/blevesearch/snowballstem v0.9.0 // indirect
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
|
||||
github.com/blevesearch/vellum v1.1.0 // indirect
|
||||
github.com/blevesearch/zapx/v11 v11.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v12 v12.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v13 v13.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v14 v14.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v15 v15.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v16 v16.2.8 // indirect
|
||||
github.com/blevesearch/zapx/v11 v11.4.1 // indirect
|
||||
github.com/blevesearch/zapx/v12 v12.4.1 // indirect
|
||||
github.com/blevesearch/zapx/v13 v13.4.1 // indirect
|
||||
github.com/blevesearch/zapx/v14 v14.4.1 // indirect
|
||||
github.com/blevesearch/zapx/v15 v15.4.1 // indirect
|
||||
github.com/blevesearch/zapx/v16 v16.2.2 // indirect
|
||||
github.com/bluele/gcache v0.0.2 // indirect
|
||||
github.com/blugelabs/ice v1.0.0 // indirect
|
||||
github.com/blugelabs/ice/v2 v2.0.1 // indirect
|
||||
@@ -443,6 +443,7 @@ require (
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
|
||||
github.com/gomodule/redigo v1.8.9 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/cel-go v0.26.1 // indirect
|
||||
|
||||
@@ -931,14 +931,14 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8=
|
||||
github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA=
|
||||
github.com/blevesearch/bleve_index_api v1.3.0 h1:DsMpWVjFNlBw9/6pyWf59XoqcAkhHj3H0UWiQsavb6E=
|
||||
github.com/blevesearch/bleve_index_api v1.3.0/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
|
||||
github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
|
||||
github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
|
||||
github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI=
|
||||
github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
|
||||
github.com/blevesearch/bleve/v2 v2.5.0 h1:HzYqBy/5/M9Ul9ESEmXzN/3Jl7YpmWBdHM/+zzv/3k4=
|
||||
github.com/blevesearch/bleve/v2 v2.5.0/go.mod h1:PcJzTPnEynO15dCf9isxOga7YFRa/cMSsbnRwnszXUk=
|
||||
github.com/blevesearch/bleve_index_api v1.2.7 h1:c8r9vmbaYQroAMSGag7zq5gEVPiuXrUQDqfnj7uYZSY=
|
||||
github.com/blevesearch/bleve_index_api v1.2.7/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
|
||||
github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
|
||||
github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=
|
||||
github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U=
|
||||
github.com/blevesearch/go-faiss v1.0.25/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
|
||||
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
|
||||
@@ -947,8 +947,8 @@ github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+
|
||||
github.com/blevesearch/mmap-go v1.0.3/go.mod h1:pYvKl/grLQrBxuaRYgoTssa4rVujYYeenDp++2E+yvs=
|
||||
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
|
||||
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.9 h1:X6nJXnNHl7nasXW+U6y2Ns2Aw8F9STszkYkyBfQ+p0o=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.9/go.mod h1:IrzspZlVjhf4X29oJiEhBxEteTqOY9RlYlk1lCmYHr4=
|
||||
github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ=
|
||||
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
|
||||
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
|
||||
@@ -960,18 +960,18 @@ github.com/blevesearch/vellum v1.0.5/go.mod h1:atE0EH3fvk43zzS7t1YNdNC7DbmcC3uz+
|
||||
github.com/blevesearch/vellum v1.0.7/go.mod h1:doBZpmRhwTsASB4QdUZANlJvqVAUdUyX0ZK7QJCTeBE=
|
||||
github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w=
|
||||
github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y=
|
||||
github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs=
|
||||
github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
|
||||
github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE=
|
||||
github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
|
||||
github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks=
|
||||
github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
|
||||
github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0=
|
||||
github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
|
||||
github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k=
|
||||
github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
|
||||
github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI=
|
||||
github.com/blevesearch/zapx/v16 v16.2.8/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14=
|
||||
github.com/blevesearch/zapx/v11 v11.4.1 h1:qFCPlFbsEdwbbckJkysptSQOsHn4s6ZOHL5GMAIAVHA=
|
||||
github.com/blevesearch/zapx/v11 v11.4.1/go.mod h1:qNOGxIqdPC1MXauJCD9HBG487PxviTUUbmChFOAosGs=
|
||||
github.com/blevesearch/zapx/v12 v12.4.1 h1:K77bhypII60a4v8mwvav7r4IxWA8qxhNjgF9xGdb9eQ=
|
||||
github.com/blevesearch/zapx/v12 v12.4.1/go.mod h1:QRPrlPOzAxBNMI0MkgdD+xsTqx65zbuPr3Ko4Re49II=
|
||||
github.com/blevesearch/zapx/v13 v13.4.1 h1:EnkEMZFUK0lsW/jOJJF2xOcp+W8TjEsyeN5BeAZEYYE=
|
||||
github.com/blevesearch/zapx/v13 v13.4.1/go.mod h1:e6duBMlCvgbH9rkzNMnUa9hRI9F7ri2BRcHfphcmGn8=
|
||||
github.com/blevesearch/zapx/v14 v14.4.1 h1:G47kGCshknBZzZAtjcnIAMn3oNx8XBLxp8DMq18ogyE=
|
||||
github.com/blevesearch/zapx/v14 v14.4.1/go.mod h1:O7sDxiaL2r2PnCXbhh1Bvm7b4sP+jp4unE9DDPWGoms=
|
||||
github.com/blevesearch/zapx/v15 v15.4.1 h1:B5IoTMUCEzFdc9FSQbhVOxAY+BO17c05866fNruiI7g=
|
||||
github.com/blevesearch/zapx/v15 v15.4.1/go.mod h1:b/MreHjYeQoLjyY2+UaM0hGZZUajEbE0xhnr1A2/Q6Y=
|
||||
github.com/blevesearch/zapx/v16 v16.2.2 h1:MifKJVRTEhMTgSlle2bDRTb39BGc9jXFRLPZc6r0Rzk=
|
||||
github.com/blevesearch/zapx/v16 v16.2.2/go.mod h1:B9Pk4G1CqtErgQV9DyCSA9Lb7WZe4olYfGw7fVDZ4sk=
|
||||
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
|
||||
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
|
||||
github.com/blugelabs/bluge v0.2.2 h1:gat8CqE6P6tOgeX30XGLOVNTC26cpM2RWVcreXWtYcM=
|
||||
@@ -1442,6 +1442,8 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2V
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||
github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
|
||||
|
||||
+2
-27
@@ -520,40 +520,14 @@ github.com/benbjohnson/immutable v0.4.0 h1:CTqXbEerYso8YzVPxmWxh2gnoRQbbB9X1quUC
|
||||
github.com/benbjohnson/immutable v0.4.0/go.mod h1:iAr8OjJGLnLmVUr9MZ/rz4PWUy6Ouc2JLYuMArmvAJM=
|
||||
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
|
||||
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
|
||||
github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8=
|
||||
github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA=
|
||||
github.com/blevesearch/bleve_index_api v1.2.8/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
|
||||
github.com/blevesearch/bleve_index_api v1.2.11 h1:bXQ54kVuwP8hdrXUSOnvTQfgK0KI1+f9A0ITJT8tX1s=
|
||||
github.com/blevesearch/bleve_index_api v1.2.11/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
|
||||
github.com/blevesearch/bleve_index_api v1.3.0 h1:DsMpWVjFNlBw9/6pyWf59XoqcAkhHj3H0UWiQsavb6E=
|
||||
github.com/blevesearch/bleve_index_api v1.3.0/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
|
||||
github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
|
||||
github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
|
||||
github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
|
||||
github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:kDy+zgJFJJoJYBvdfBSiZYBbdsUL0XcjHYWezpQBGPA=
|
||||
github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:9eJDeqxJ3E7WnLebQUlPD7ZjSce7AnDb9vjGmMCbD0A=
|
||||
github.com/blevesearch/goleveldb v1.0.1 h1:iAtV2Cu5s0GD1lwUiekkFHe2gTMCCNVj2foPclDLIFI=
|
||||
github.com/blevesearch/goleveldb v1.0.1/go.mod h1:WrU8ltZbIp0wAoig/MHbrPCXSOLpe79nz5lv5nqfYrQ=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.10/go.mod h1:Z3e6ChN3qyN35yaQpl00MfI5s8AxUJbpTR/DL8QOQ+8=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc=
|
||||
github.com/blevesearch/snowball v0.6.1 h1:cDYjn/NCH+wwt2UdehaLpr2e4BwLIjN4V/TdLsL+B5A=
|
||||
github.com/blevesearch/snowball v0.6.1/go.mod h1:ZF0IBg5vgpeoUhnMza2v0A/z8m1cWPlwhke08LpNusg=
|
||||
github.com/blevesearch/stempel v0.2.0 h1:CYzVPaScODMvgE9o+kf6D4RJ/VRomyi9uHF+PtB+Afc=
|
||||
github.com/blevesearch/stempel v0.2.0/go.mod h1:wjeTHqQv+nQdbPuJ/YcvOjTInA2EIc6Ks1FoSUzSLvc=
|
||||
github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k=
|
||||
github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs=
|
||||
github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
|
||||
github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE=
|
||||
github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
|
||||
github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks=
|
||||
github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
|
||||
github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0=
|
||||
github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
|
||||
github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k=
|
||||
github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
|
||||
github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI=
|
||||
github.com/blevesearch/zapx/v16 v16.2.8/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14=
|
||||
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
|
||||
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
|
||||
@@ -1024,6 +998,8 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975/go.mod h1:FGdGvhI40Dq+CTQaSzK9evuve774cgOUdGfVO04OXkw=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36 h1:AjZ58JRw1ZieFH/SdsddF5BXtsDKt5kSrKNPWrzYz3Y=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f h1:9tRhudagkQO2s61SLFLSziIdCm7XlkfypVKDxpcHokg=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f/go.mod h1:AsVdCBeDFN9QbgpJg+8voDAcgsW0RmNvBd70ecMMdC0=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae/go.mod h1:6CJ1uXmLZ13ufpO9xE4pST+DyaBt0uszzrV0YnoaVLQ=
|
||||
github.com/grafana/sqlds/v4 v4.2.4/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
@@ -1116,7 +1092,6 @@ github.com/jon-whit/go-grpc-prometheus v1.4.0/go.mod h1:iTPm+Iuhh3IIqR0iGZ91JJEg
|
||||
github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a h1:sfe532Ipn7GX0V6mHdynBk393rDmqgI0QmjLK7ct7TU=
|
||||
github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a/go.mod h1:dNKs71rs2VJGBAmttu7fouEsRQlRjxy0p1Sx+T5wbpY=
|
||||
github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY=
|
||||
github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
|
||||
github.com/jsternberg/zap-logfmt v1.3.0 h1:z1n1AOHVVydOOVuyphbOKyR4NICDQFiJMn1IK5hVQ5Y=
|
||||
github.com/jsternberg/zap-logfmt v1.3.0/go.mod h1:N3DENp9WNmCZxvkBD/eReWwz1149BK6jEN9cQ4fNwZE=
|
||||
|
||||
@@ -82,7 +82,6 @@ module.exports = {
|
||||
// Decoupled plugins run their own tests so ignoring them here.
|
||||
'<rootDir>/public/app/plugins/datasource/azuremonitor',
|
||||
'<rootDir>/public/app/plugins/datasource/cloud-monitoring',
|
||||
'<rootDir>/public/app/plugins/datasource/elasticsearch',
|
||||
'<rootDir>/public/app/plugins/datasource/grafana-postgresql-datasource',
|
||||
'<rootDir>/public/app/plugins/datasource/grafana-pyroscope-datasource',
|
||||
'<rootDir>/public/app/plugins/datasource/grafana-testdata-datasource',
|
||||
|
||||
@@ -35,14 +35,6 @@
|
||||
},
|
||||
"./test": {
|
||||
"@grafana-app/source": "./test/index.ts"
|
||||
},
|
||||
"./themes/schema.generated.json": {
|
||||
"@grafana-app/source": "./src/themes/schema.generated.json",
|
||||
"default": "./dist/esm/themes/schema.generated.json"
|
||||
},
|
||||
"./themes/definitions/*.json": {
|
||||
"@grafana-app/source": "./src/themes/themeDefinitions/*.json",
|
||||
"default": "./dist/esm/themes/themeDefinitions/*.json"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
@@ -60,7 +52,7 @@
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
|
||||
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
|
||||
"postpack": "mv package.json.bak package.json",
|
||||
"themes-schema": "tsx ./scripts/generateSchema.ts"
|
||||
"themes-schema": "tsx ./src/themes/scripts/generateSchema.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "7.0.1",
|
||||
@@ -110,7 +102,6 @@
|
||||
"react-dom": "18.3.1",
|
||||
"rimraf": "6.0.1",
|
||||
"rollup": "^4.22.4",
|
||||
"rollup-plugin-copy": "3.5.0",
|
||||
"rollup-plugin-esbuild": "6.2.1",
|
||||
"rollup-plugin-node-externals": "^8.0.0",
|
||||
"tsx": "^4.21.0",
|
||||
|
||||
@@ -1,40 +1,21 @@
|
||||
import json from '@rollup/plugin-json';
|
||||
import { createRequire } from 'node:module';
|
||||
import copy from 'rollup-plugin-copy';
|
||||
|
||||
import { entryPoint, plugins, esmOutput, cjsOutput } from '../rollup.config.parts';
|
||||
|
||||
const rq = createRequire(import.meta.url);
|
||||
const pkg = rq('./package.json');
|
||||
|
||||
const grafanaDataPlugins = [
|
||||
...plugins,
|
||||
copy({
|
||||
targets: [
|
||||
{
|
||||
src: 'src/themes/schema.generated.json',
|
||||
dest: 'dist/esm/',
|
||||
},
|
||||
{
|
||||
src: 'src/themes/themeDefinitions/*.json',
|
||||
dest: 'dist/esm/',
|
||||
},
|
||||
],
|
||||
flatten: false,
|
||||
}),
|
||||
json(),
|
||||
];
|
||||
|
||||
export default [
|
||||
{
|
||||
input: entryPoint,
|
||||
plugins: grafanaDataPlugins,
|
||||
plugins: [...plugins, json()],
|
||||
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
|
||||
treeshake: false,
|
||||
},
|
||||
{
|
||||
input: 'src/unstable.ts',
|
||||
plugins: grafanaDataPlugins,
|
||||
plugins: [...plugins, json()],
|
||||
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
|
||||
treeshake: false,
|
||||
},
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { NewThemeOptionsSchema } from '../src/themes/createTheme';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const jsonOut = path.join(__dirname, '..', 'src', 'themes', 'schema.generated.json');
|
||||
|
||||
fs.writeFileSync(
|
||||
jsonOut,
|
||||
JSON.stringify(
|
||||
NewThemeOptionsSchema.toJSONSchema({
|
||||
target: 'draft-07',
|
||||
}),
|
||||
undefined,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
console.log('Successfully generated theme schema');
|
||||
@@ -93,6 +93,7 @@ export { DataTransformerID } from '../transformations/transformers/ids';
|
||||
|
||||
export { mergeTransformer } from '../transformations/transformers/merge';
|
||||
export { getThemeById } from '../themes/registry';
|
||||
export * as experimentalThemeDefinitions from '../themes/themeDefinitions';
|
||||
export { GrafanaEdition } from '../types/config';
|
||||
export { SIPrefix } from '../valueFormats/symbolFormatters';
|
||||
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
import { Registry, RegistryItem } from '../utils/Registry';
|
||||
|
||||
import { createTheme, NewThemeOptionsSchema } from './createTheme';
|
||||
import aubergine from './themeDefinitions/aubergine.json';
|
||||
import debug from './themeDefinitions/debug.json';
|
||||
import desertbloom from './themeDefinitions/desertbloom.json';
|
||||
import gildedgrove from './themeDefinitions/gildedgrove.json';
|
||||
import gloom from './themeDefinitions/gloom.json';
|
||||
import mars from './themeDefinitions/mars.json';
|
||||
import matrix from './themeDefinitions/matrix.json';
|
||||
import sapphiredusk from './themeDefinitions/sapphiredusk.json';
|
||||
import synthwave from './themeDefinitions/synthwave.json';
|
||||
import tron from './themeDefinitions/tron.json';
|
||||
import victorian from './themeDefinitions/victorian.json';
|
||||
import zen from './themeDefinitions/zen.json';
|
||||
import * as extraThemes from './themeDefinitions';
|
||||
import { GrafanaTheme2 } from './types';
|
||||
|
||||
export interface ThemeRegistryItem extends RegistryItem {
|
||||
@@ -20,21 +9,6 @@ export interface ThemeRegistryItem extends RegistryItem {
|
||||
build: () => GrafanaTheme2;
|
||||
}
|
||||
|
||||
const extraThemes: { [key: string]: unknown } = {
|
||||
aubergine,
|
||||
debug,
|
||||
desertbloom,
|
||||
gildedgrove,
|
||||
gloom,
|
||||
mars,
|
||||
matrix,
|
||||
sapphiredusk,
|
||||
synthwave,
|
||||
tron,
|
||||
victorian,
|
||||
zen,
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Only for internal use, never use this from a plugin
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { NewThemeOptionsSchema } from '../createTheme';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, '../schema.generated.json'),
|
||||
JSON.stringify(
|
||||
NewThemeOptionsSchema.toJSONSchema({
|
||||
target: 'draft-07',
|
||||
}),
|
||||
undefined,
|
||||
2
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
export { default as aubergine } from './aubergine.json';
|
||||
export { default as debug } from './debug.json';
|
||||
export { default as desertbloom } from './desertbloom.json';
|
||||
export { default as gildedgrove } from './gildedgrove.json';
|
||||
export { default as mars } from './mars.json';
|
||||
export { default as matrix } from './matrix.json';
|
||||
export { default as sapphiredusk } from './sapphiredusk.json';
|
||||
export { default as synthwave } from './synthwave.json';
|
||||
export { default as tron } from './tron.json';
|
||||
export { default as victorian } from './victorian.json';
|
||||
export { default as zen } from './zen.json';
|
||||
export { default as gloom } from './gloom.json';
|
||||
@@ -9,4 +9,4 @@
|
||||
* and be subject to the standard policies
|
||||
*/
|
||||
|
||||
export {};
|
||||
export { default as themeJsonSchema } from './themes/schema.generated.json';
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
"emitDeclarationOnly": true,
|
||||
"isolatedModules": true,
|
||||
"rootDirs": ["."],
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true
|
||||
"moduleResolution": "bundler"
|
||||
},
|
||||
"exclude": ["dist/**/*"],
|
||||
"include": [
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
|
||||
import * as common from '@grafana/schema';
|
||||
|
||||
export const pluginVersion = "%VERSION%";
|
||||
export const pluginVersion = "12.4.0-pre";
|
||||
|
||||
export type BucketAggregation = (DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
iamauthorizer "github.com/grafana/grafana/pkg/registry/apis/iam/authorizer"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
|
||||
@@ -40,6 +41,22 @@ func newIAMAuthorizer(
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
})
|
||||
|
||||
serviceIdentityAuthorizer := authorizer.AuthorizerFunc(func(
|
||||
ctx context.Context, attr authorizer.Attributes,
|
||||
) (authorized authorizer.Decision, reason string, err error) {
|
||||
if identity.IsServiceIdentity(ctx) {
|
||||
// A Grafana sub-system should have full access. We trust them to make wise decisions.
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
req, err := identity.GetRequester(ctx)
|
||||
if err == nil && req != nil && req.GetIsGrafanaAdmin() {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
return authorizer.DecisionDeny, "", nil
|
||||
})
|
||||
|
||||
// Identity specific resources
|
||||
legacyAuthorizer := gfauthorizer.NewResourceAuthorizer(legacyAccessClient)
|
||||
resourceAuthorizer["display"] = legacyAuthorizer
|
||||
@@ -57,6 +74,8 @@ func newIAMAuthorizer(
|
||||
resourceAuthorizer[iamv0.TeamBindingResourceInfo.GetName()] = allowAuthorizer
|
||||
resourceAuthorizer["searchUsers"] = serviceAuthorizer
|
||||
resourceAuthorizer["searchTeams"] = serviceAuthorizer
|
||||
// TODO: Implement fine-grained authorization for external group mapping search on the search level
|
||||
resourceAuthorizer["searchExternalGroupMappings"] = serviceIdentityAuthorizer
|
||||
|
||||
return &iamAuthorizer{resourceAuthorizer: resourceAuthorizer}
|
||||
}
|
||||
|
||||
@@ -97,8 +97,8 @@ func (r *TeamBindingAuthorizer) beforeWrite(ctx context.Context, obj runtime.Obj
|
||||
teamName := concreteObj.Spec.TeamRef.Name
|
||||
checkReq := types.CheckRequest{
|
||||
Namespace: authInfo.GetNamespace(),
|
||||
Group: iamv0.GROUP,
|
||||
Resource: iamv0.TeamResourceInfo.GetName(),
|
||||
Group: iamv0.TeamResourceInfo.GroupResource().Group,
|
||||
Resource: iamv0.TeamResourceInfo.GroupResource().Resource,
|
||||
Verb: utils.VerbSetPermissions,
|
||||
Name: teamName,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package externalgroupmapping
|
||||
|
||||
import "k8s.io/apiserver/pkg/registry/rest"
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/kube-openapi/pkg/common"
|
||||
)
|
||||
|
||||
type TeamGroupsHandler interface {
|
||||
rest.Storage
|
||||
@@ -8,3 +12,7 @@ type TeamGroupsHandler interface {
|
||||
rest.StorageMetadata
|
||||
rest.Connecter
|
||||
}
|
||||
|
||||
type SearchHandler interface {
|
||||
GetAPIRoutes(defs map[string]common.OpenAPIDefinition) *builder.APIRoutes
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package externalgroupmapping
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/util/errhttp"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/kube-openapi/pkg/common"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
)
|
||||
|
||||
var _ SearchHandler = (*NoopSearchREST)(nil)
|
||||
|
||||
type NoopSearchREST struct{}
|
||||
|
||||
func ProvideNoopSearchREST() *NoopSearchREST {
|
||||
return &NoopSearchREST{}
|
||||
}
|
||||
|
||||
func (n *NoopSearchREST) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) *builder.APIRoutes {
|
||||
searchResults := defs["github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingList"].Schema
|
||||
return &builder.APIRoutes{
|
||||
Namespace: []builder.APIRouteHandler{
|
||||
{
|
||||
Path: "searchExternalGroupMappings",
|
||||
Spec: &spec3.PathProps{
|
||||
Post: &spec3.Operation{
|
||||
OperationProps: spec3.OperationProps{
|
||||
Description: "External Group Mapping search",
|
||||
Tags: []string{"Search"},
|
||||
OperationId: "searchExternalGroupMappings",
|
||||
RequestBody: &spec3.RequestBody{
|
||||
RequestBodyProps: spec3.RequestBodyProps{
|
||||
Content: map[string]*spec3.MediaType{
|
||||
"application/json": {
|
||||
MediaTypeProps: spec3.MediaTypeProps{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"externalGroups": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Parameters: []*spec3.Parameter{
|
||||
{
|
||||
ParameterProps: spec3.ParameterProps{
|
||||
Name: "namespace",
|
||||
In: "path",
|
||||
Required: true,
|
||||
Example: "default",
|
||||
Description: "workspace",
|
||||
Schema: spec.StringProperty(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ParameterProps: spec3.ParameterProps{
|
||||
Name: "teamName",
|
||||
In: "query",
|
||||
Required: false,
|
||||
Description: "Team name",
|
||||
Schema: spec.StringProperty(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ParameterProps: spec3.ParameterProps{
|
||||
Name: "limit",
|
||||
In: "query",
|
||||
Description: "number of results to return",
|
||||
Example: 30,
|
||||
Required: false,
|
||||
Schema: spec.Int64Property(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ParameterProps: spec3.ParameterProps{
|
||||
Name: "page",
|
||||
In: "query",
|
||||
Description: "page number (starting from 1)",
|
||||
Example: 1,
|
||||
Required: false,
|
||||
Schema: spec.Int64Property(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ParameterProps: spec3.ParameterProps{
|
||||
Name: "offset",
|
||||
In: "query",
|
||||
Description: "number of results to skip",
|
||||
Example: 0,
|
||||
Required: false,
|
||||
Schema: spec.Int64Property(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ParameterProps: spec3.ParameterProps{
|
||||
Name: "sort",
|
||||
In: "query",
|
||||
Description: "sortable field",
|
||||
Example: "",
|
||||
Examples: map[string]*spec3.Example{
|
||||
"externalGroup": {
|
||||
ExampleProps: spec3.ExampleProps{
|
||||
Summary: "externalGroup ascending",
|
||||
Value: "externalGroup",
|
||||
},
|
||||
},
|
||||
"-externalGroup": {
|
||||
ExampleProps: spec3.ExampleProps{
|
||||
Summary: "externalGroup descending",
|
||||
Value: "-externalGroup",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: false,
|
||||
Schema: spec.StringProperty(),
|
||||
},
|
||||
},
|
||||
},
|
||||
Responses: &spec3.Responses{
|
||||
ResponsesProps: spec3.ResponsesProps{
|
||||
Default: &spec3.Response{
|
||||
ResponseProps: spec3.ResponseProps{
|
||||
Description: "Default OK response",
|
||||
Content: map[string]*spec3.MediaType{
|
||||
"application/json": {
|
||||
MediaTypeProps: spec3.MediaTypeProps{
|
||||
Schema: &searchResults,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Handler: n.doSearch,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NoopSearchREST) doSearch(w http.ResponseWriter, r *http.Request) {
|
||||
errhttp.Write(r.Context(), errors.NewForbidden(iamv0.ExternalGroupMappingResourceInfo.GroupResource(), "", fmt.Errorf("functionality not available")), w)
|
||||
}
|
||||
@@ -82,11 +82,12 @@ type IdentityAccessManagementAPIBuilder struct {
|
||||
reg prometheus.Registerer
|
||||
logger log.Logger
|
||||
|
||||
dual dualwrite.Service
|
||||
unified resource.ResourceClient
|
||||
userSearchClient resourcepb.ResourceIndexClient
|
||||
userSearchHandler *user.SearchHandler
|
||||
teamSearch *TeamSearchHandler
|
||||
dual dualwrite.Service
|
||||
unified resource.ResourceClient
|
||||
userSearchClient resourcepb.ResourceIndexClient
|
||||
userSearchHandler *user.SearchHandler
|
||||
teamSearch *TeamSearchHandler
|
||||
externalGroupMappingSearchHandler externalgroupmapping.SearchHandler
|
||||
|
||||
teamGroupsHandler externalgroupmapping.TeamGroupsHandler
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ func RegisterAPIService(
|
||||
roleBindingsStorage RoleBindingStorageBackend,
|
||||
externalGroupMappingStorageBackend ExternalGroupMappingStorageBackend,
|
||||
teamGroupsHandlerImpl externalgroupmapping.TeamGroupsHandler,
|
||||
externalGroupMappingSearchHandler externalgroupmapping.SearchHandler,
|
||||
dual dualwrite.Service,
|
||||
unified resource.ResourceClient,
|
||||
orgService org.Service,
|
||||
@@ -101,31 +102,32 @@ func RegisterAPIService(
|
||||
)
|
||||
|
||||
builder := &IdentityAccessManagementAPIBuilder{
|
||||
store: store,
|
||||
userLegacyStore: user.NewLegacyStore(store, accessClient, enableAuthnMutation, tracing),
|
||||
saLegacyStore: serviceaccount.NewLegacyStore(store, accessClient, enableAuthnMutation, tracing),
|
||||
legacyTeamStore: team.NewLegacyStore(store, accessClient, enableAuthnMutation, tracing),
|
||||
teamBindingLegacyStore: teambinding.NewLegacyBindingStore(store, enableAuthnMutation, tracing),
|
||||
ssoLegacyStore: sso.NewLegacyStore(ssoService, tracing),
|
||||
coreRolesStorage: coreRolesStorage,
|
||||
roleApiInstaller: roleApiInstaller,
|
||||
resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider),
|
||||
roleBindingsStorage: roleBindingsStorage,
|
||||
externalGroupMappingStorage: externalGroupMappingStorageBackend,
|
||||
teamGroupsHandler: teamGroupsHandlerImpl,
|
||||
sso: ssoService,
|
||||
resourceParentProvider: resourceParentProvider,
|
||||
authorizer: authorizer,
|
||||
legacyAccessClient: legacyAccessClient,
|
||||
accessClient: accessClient,
|
||||
zClient: zClient,
|
||||
zTickets: make(chan bool, MaxConcurrentZanzanaWrites),
|
||||
display: user.NewLegacyDisplayREST(store),
|
||||
reg: reg,
|
||||
logger: log.New("iam.apis"),
|
||||
features: features,
|
||||
dual: dual,
|
||||
unified: unified,
|
||||
store: store,
|
||||
userLegacyStore: user.NewLegacyStore(store, accessClient, enableAuthnMutation, tracing),
|
||||
saLegacyStore: serviceaccount.NewLegacyStore(store, accessClient, enableAuthnMutation, tracing),
|
||||
legacyTeamStore: team.NewLegacyStore(store, accessClient, enableAuthnMutation, tracing),
|
||||
teamBindingLegacyStore: teambinding.NewLegacyBindingStore(store, enableAuthnMutation, tracing),
|
||||
ssoLegacyStore: sso.NewLegacyStore(ssoService, tracing),
|
||||
coreRolesStorage: coreRolesStorage,
|
||||
roleApiInstaller: roleApiInstaller,
|
||||
resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider),
|
||||
roleBindingsStorage: roleBindingsStorage,
|
||||
externalGroupMappingStorage: externalGroupMappingStorageBackend,
|
||||
teamGroupsHandler: teamGroupsHandlerImpl,
|
||||
externalGroupMappingSearchHandler: externalGroupMappingSearchHandler,
|
||||
sso: ssoService,
|
||||
resourceParentProvider: resourceParentProvider,
|
||||
authorizer: authorizer,
|
||||
legacyAccessClient: legacyAccessClient,
|
||||
accessClient: accessClient,
|
||||
zClient: zClient,
|
||||
zTickets: make(chan bool, MaxConcurrentZanzanaWrites),
|
||||
display: user.NewLegacyDisplayREST(store),
|
||||
reg: reg,
|
||||
logger: log.New("iam.apis"),
|
||||
features: features,
|
||||
dual: dual,
|
||||
unified: unified,
|
||||
userSearchClient: resource.NewSearchClient(dualwrite.NewSearchAdapter(dual), iamv0.UserResourceInfo.GroupResource(),
|
||||
unified, user.NewUserLegacySearchClient(orgService, tracing, cfg), features),
|
||||
teamSearch: NewTeamSearchHandler(tracing, dual, team.NewLegacyTeamSearchClient(teamService), unified, features),
|
||||
@@ -625,7 +627,7 @@ func (b *IdentityAccessManagementAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenA
|
||||
func (b *IdentityAccessManagementAPIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIRoutes {
|
||||
defs := b.GetOpenAPIDefinitions()(func(path string) spec.Ref { return spec.Ref{} })
|
||||
|
||||
searchRoutes := make([]*builder.APIRoutes, 0, 2)
|
||||
searchRoutes := make([]*builder.APIRoutes, 0, 3)
|
||||
if b.userSearchHandler != nil {
|
||||
searchRoutes = append(searchRoutes, b.userSearchHandler.GetAPIRoutes(defs))
|
||||
}
|
||||
@@ -634,6 +636,10 @@ func (b *IdentityAccessManagementAPIBuilder) GetAPIRoutes(gv schema.GroupVersion
|
||||
searchRoutes = append(searchRoutes, b.teamSearch.GetAPIRoutes(defs))
|
||||
}
|
||||
|
||||
if b.externalGroupMappingSearchHandler != nil {
|
||||
searchRoutes = append(searchRoutes, b.externalGroupMappingSearchHandler.GetAPIRoutes(defs))
|
||||
}
|
||||
|
||||
routes := []*builder.APIRoutes{b.display.GetAPIRoutes(defs)}
|
||||
routes = append(routes, searchRoutes...)
|
||||
return mergeAPIRoutes(routes...)
|
||||
|
||||
@@ -35,6 +35,9 @@ var WireSetExts = wire.NewSet(
|
||||
externalgroupmapping.ProvideNoopTeamGroupsREST,
|
||||
wire.Bind(new(externalgroupmapping.TeamGroupsHandler), new(*externalgroupmapping.NoopTeamGroupsREST)),
|
||||
|
||||
externalgroupmapping.ProvideNoopSearchREST,
|
||||
wire.Bind(new(externalgroupmapping.SearchHandler), new(*externalgroupmapping.NoopSearchREST)),
|
||||
|
||||
// Auditing Options
|
||||
auditing.ProvideNoopBackend,
|
||||
auditing.ProvideNoopPolicyRuleProvider,
|
||||
|
||||
Generated
+4
-2
@@ -883,7 +883,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
storageBackendImpl := noopstorage.ProvideStorageBackend()
|
||||
roleApiInstaller := iam.ProvideNoopRoleApiInstaller()
|
||||
noopTeamGroupsREST := externalgroupmapping.ProvideNoopTeamGroupsREST()
|
||||
identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(cfg, featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, roleApiInstaller, tracingService, storageBackendImpl, storageBackendImpl, noopTeamGroupsREST, dualwriteService, resourceClient, orgService, userService, teamService, eventualRestConfigProvider)
|
||||
noopSearchREST := externalgroupmapping.ProvideNoopSearchREST()
|
||||
identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(cfg, featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, roleApiInstaller, tracingService, storageBackendImpl, storageBackendImpl, noopTeamGroupsREST, noopSearchREST, dualwriteService, resourceClient, orgService, userService, teamService, eventualRestConfigProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1551,7 +1552,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
storageBackendImpl := noopstorage.ProvideStorageBackend()
|
||||
roleApiInstaller := iam.ProvideNoopRoleApiInstaller()
|
||||
noopTeamGroupsREST := externalgroupmapping.ProvideNoopTeamGroupsREST()
|
||||
identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(cfg, featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, roleApiInstaller, tracingService, storageBackendImpl, storageBackendImpl, noopTeamGroupsREST, dualwriteService, resourceClient, orgService, userService, teamService, eventualRestConfigProvider)
|
||||
noopSearchREST := externalgroupmapping.ProvideNoopSearchREST()
|
||||
identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(cfg, featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, roleApiInstaller, tracingService, storageBackendImpl, storageBackendImpl, noopTeamGroupsREST, noopSearchREST, dualwriteService, resourceClient, orgService, userService, teamService, eventualRestConfigProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1898,7 +1898,7 @@ func (q *permissionScopedQuery) Searcher(ctx context.Context, i index.IndexReade
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filteringSearcher := bleveSearch.NewFilteringSearcher(ctx, searcher, func(_ *search.SearchContext, d *search.DocumentMatch) bool {
|
||||
filteringSearcher := bleveSearch.NewFilteringSearcher(ctx, searcher, func(d *search.DocumentMatch) bool {
|
||||
// The doc ID has the format: <namespace>/<group>/<resourceType>/<name>
|
||||
// IndexInternalID will be the same as the doc ID when using an in-memory index, but when using a file-based
|
||||
// index it becomes a binary encoded number that has some other internal meaning. Using ExternalID() will get the
|
||||
|
||||
@@ -24,24 +24,6 @@ func TestMain(m *testing.M) {
|
||||
testsuite.Run(m)
|
||||
}
|
||||
|
||||
// mockElasticsearchHandler returns a handler that mocks Elasticsearch endpoints.
|
||||
// It responds to GET / with cluster info (required for datasource initialization)
|
||||
// and returns 401 Unauthorized for all other requests.
|
||||
func mockElasticsearchHandler(onRequest func(r *http.Request)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"version":{"build_flavor":"default","number":"8.0.0"}}`))
|
||||
default:
|
||||
if onRequest != nil {
|
||||
onRequest(r)
|
||||
}
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationElasticsearch(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
@@ -53,8 +35,9 @@ func TestIntegrationElasticsearch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
var outgoingRequest *http.Request
|
||||
outgoingServer := httptest.NewServer(mockElasticsearchHandler(func(r *http.Request) {
|
||||
outgoingServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
outgoingRequest = r
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}))
|
||||
t.Cleanup(outgoingServer.Close)
|
||||
|
||||
|
||||
@@ -639,7 +639,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "\u003e=11.6.0",
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
|
||||
@@ -912,6 +912,139 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"/apis/iam.grafana.app/v0alpha1/namespaces/{namespace}/searchExternalGroupMappings": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Search"
|
||||
],
|
||||
"description": "External Group Mapping search",
|
||||
"operationId": "searchExternalGroupMappings",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "namespace",
|
||||
"in": "path",
|
||||
"description": "workspace",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "default"
|
||||
},
|
||||
{
|
||||
"name": "teamName",
|
||||
"in": "query",
|
||||
"description": "Team name",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"description": "number of results to return",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"example": 30
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"description": "page number (starting from 1)",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"example": 1
|
||||
},
|
||||
{
|
||||
"name": "offset",
|
||||
"in": "query",
|
||||
"description": "number of results to skip",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"example": 0
|
||||
},
|
||||
{
|
||||
"name": "sort",
|
||||
"in": "query",
|
||||
"description": "sortable field",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": {
|
||||
"": {
|
||||
"summary": "default sorting",
|
||||
"value": "externalGroup"
|
||||
},
|
||||
"-externalGroup": {
|
||||
"summary": "externalGroup descending",
|
||||
"value": "-externalGroup"
|
||||
},
|
||||
"externalGroup": {
|
||||
"summary": "externalGroup ascending",
|
||||
"value": "externalGroup"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"externalGroups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"default": {
|
||||
"description": "Default OK response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"metadata",
|
||||
"items"
|
||||
],
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"default": {}
|
||||
}
|
||||
},
|
||||
"kind": {
|
||||
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"default": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/apis/iam.grafana.app/v0alpha1/namespaces/{namespace}/searchTeams": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
||||
@@ -92,7 +92,7 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
|
||||
}, nil
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v3/projects/%s/metricDescriptors", dsInfo.services[cloudMonitor].url, defaultProject)
|
||||
url := fmt.Sprintf("%v/v3/projects/%v/metricDescriptors", dsInfo.services[cloudMonitor].url, defaultProject)
|
||||
request, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -139,7 +139,6 @@ type datasourceInfo struct {
|
||||
defaultProject string
|
||||
clientEmail string
|
||||
tokenUri string
|
||||
universeDomain string
|
||||
services map[string]datasourceService
|
||||
privateKey string
|
||||
usingImpersonation bool
|
||||
@@ -151,7 +150,6 @@ type datasourceJSONData struct {
|
||||
DefaultProject string `json:"defaultProject"`
|
||||
ClientEmail string `json:"clientEmail"`
|
||||
TokenURI string `json:"tokenUri"`
|
||||
UniverseDomain string `json:"universeDomain"`
|
||||
UsingImpersonation bool `json:"usingImpersonation"`
|
||||
ServiceAccountToImpersonate string `json:"serviceAccountToImpersonate"`
|
||||
}
|
||||
@@ -181,7 +179,6 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
|
||||
defaultProject: jsonData.DefaultProject,
|
||||
clientEmail: jsonData.ClientEmail,
|
||||
tokenUri: jsonData.TokenURI,
|
||||
universeDomain: jsonData.UniverseDomain,
|
||||
usingImpersonation: jsonData.UsingImpersonation,
|
||||
serviceAccountToImpersonate: jsonData.ServiceAccountToImpersonate,
|
||||
services: map[string]datasourceService{},
|
||||
@@ -197,13 +194,13 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for name := range routes {
|
||||
for name, info := range routes {
|
||||
client, err := newHTTPClient(dsInfo, opts, &httpClientProvider, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dsInfo.services[name] = datasourceService{
|
||||
url: buildURL(name, dsInfo.universeDomain),
|
||||
url: info.url,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ type routeInfo struct {
|
||||
var routes = map[string]routeInfo{
|
||||
cloudMonitor: {
|
||||
method: "GET",
|
||||
url: "https://monitoring.",
|
||||
url: "https://monitoring.googleapis.com",
|
||||
scopes: []string{cloudMonitorScope},
|
||||
},
|
||||
resourceManager: {
|
||||
method: "GET",
|
||||
url: "https://cloudresourcemanager.",
|
||||
url: "https://cloudresourcemanager.googleapis.com",
|
||||
scopes: []string{resourceManagerScope},
|
||||
},
|
||||
}
|
||||
@@ -68,13 +68,6 @@ func getMiddleware(model *datasourceInfo, routePath string) (httpclient.Middlewa
|
||||
return tokenprovider.AuthMiddleware(provider), nil
|
||||
}
|
||||
|
||||
func buildURL(route string, universeDomain string) string {
|
||||
if universeDomain == "" {
|
||||
universeDomain = "googleapis.com"
|
||||
}
|
||||
return routes[route].url + universeDomain
|
||||
}
|
||||
|
||||
func newHTTPClient(model *datasourceInfo, opts httpclient.Options, clientProvider *httpclient.Provider, route string) (*http.Client, error) {
|
||||
m, err := getMiddleware(model, route)
|
||||
if err != nil {
|
||||
|
||||
@@ -111,7 +111,7 @@ func Test_setRequestVariables(t *testing.T) {
|
||||
im: &fakeInstance{
|
||||
services: map[string]datasourceService{
|
||||
cloudMonitor: {
|
||||
url: buildURL(cloudMonitor, "googleapis.com"),
|
||||
url: routes[cloudMonitor].url,
|
||||
client: &http.Client{},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,8 +3,8 @@ package elasticsearch
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
// addDateHistogramAgg adds a date histogram aggregation to the aggregation builder
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
// Used in logging to mark a stage
|
||||
@@ -34,7 +35,6 @@ type DatasourceInfo struct {
|
||||
Interval string
|
||||
MaxConcurrentShardRequests int64
|
||||
IncludeFrozen bool
|
||||
ClusterInfo ClusterInfo
|
||||
}
|
||||
|
||||
type ConfiguredFields struct {
|
||||
@@ -159,7 +159,7 @@ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearch
|
||||
resSpan.End()
|
||||
}()
|
||||
|
||||
improvedParsingEnabled := isFeatureEnabled(c.ctx, "elasticsearchImprovedParsing")
|
||||
improvedParsingEnabled := isFeatureEnabled(c.ctx, featuremgmt.FlagElasticsearchImprovedParsing)
|
||||
msr, err := c.parser.parseMultiSearchResponse(res.Body, improvedParsingEnabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -197,11 +197,7 @@ func (c *baseClientImpl) createMultiSearchRequests(searchRequests []*SearchReque
|
||||
|
||||
func (c *baseClientImpl) getMultiSearchQueryParameters() string {
|
||||
var qs []string
|
||||
// if the build flavor is not serverless, we can use the max concurrent shard requests
|
||||
// this is because serverless clusters do not support max concurrent shard requests
|
||||
if !c.ds.ClusterInfo.IsServerless() && c.ds.MaxConcurrentShardRequests > 0 {
|
||||
qs = append(qs, fmt.Sprintf("max_concurrent_shard_requests=%d", c.ds.MaxConcurrentShardRequests))
|
||||
}
|
||||
qs = append(qs, fmt.Sprintf("max_concurrent_shard_requests=%d", c.ds.MaxConcurrentShardRequests))
|
||||
|
||||
if c.ds.IncludeFrozen {
|
||||
qs = append(qs, "ignore_throttled=false")
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
func TestClient_ExecuteMultisearch(t *testing.T) {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package es
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type VersionInfo struct {
|
||||
BuildFlavor string `json:"build_flavor"`
|
||||
}
|
||||
|
||||
// ClusterInfo represents Elasticsearch cluster information returned from the root endpoint.
|
||||
// It is used to determine cluster capabilities and configuration like whether the cluster is serverless.
|
||||
type ClusterInfo struct {
|
||||
Version VersionInfo `json:"version"`
|
||||
}
|
||||
|
||||
const (
|
||||
BuildFlavorServerless = "serverless"
|
||||
)
|
||||
|
||||
// GetClusterInfo fetches cluster information from the Elasticsearch root endpoint.
|
||||
// It returns the cluster build flavor which is used to determine if the cluster is serverless.
|
||||
func GetClusterInfo(httpCli *http.Client, url string) (clusterInfo ClusterInfo, err error) {
|
||||
resp, err := httpCli.Get(url)
|
||||
if err != nil {
|
||||
return ClusterInfo{}, fmt.Errorf("error getting ES cluster info: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return ClusterInfo{}, fmt.Errorf("unexpected status code %d getting ES cluster info", resp.StatusCode)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if closeErr := resp.Body.Close(); closeErr != nil && err == nil {
|
||||
err = fmt.Errorf("error closing response body: %w", closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&clusterInfo)
|
||||
if err != nil {
|
||||
return ClusterInfo{}, fmt.Errorf("error decoding ES cluster info: %w", err)
|
||||
}
|
||||
|
||||
return clusterInfo, nil
|
||||
}
|
||||
|
||||
func (ci ClusterInfo) IsServerless() bool {
|
||||
return ci.Version.BuildFlavor == BuildFlavorServerless
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
package es
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetClusterInfo(t *testing.T) {
|
||||
t.Run("Should successfully get cluster info", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
_, err := rw.Write([]byte(`{
|
||||
"name": "test-cluster",
|
||||
"cluster_name": "elasticsearch",
|
||||
"cluster_uuid": "abc123",
|
||||
"version": {
|
||||
"number": "8.0.0",
|
||||
"build_flavor": "default",
|
||||
"build_type": "tar",
|
||||
"build_hash": "abc123",
|
||||
"build_date": "2023-01-01T00:00:00.000Z",
|
||||
"build_snapshot": false,
|
||||
"lucene_version": "9.0.0"
|
||||
}
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, clusterInfo)
|
||||
assert.Equal(t, "default", clusterInfo.Version.BuildFlavor)
|
||||
})
|
||||
|
||||
t.Run("Should successfully get serverless cluster info", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
_, err := rw.Write([]byte(`{
|
||||
"name": "serverless-cluster",
|
||||
"cluster_name": "elasticsearch",
|
||||
"cluster_uuid": "def456",
|
||||
"version": {
|
||||
"number": "8.11.0",
|
||||
"build_flavor": "serverless",
|
||||
"build_type": "docker",
|
||||
"build_hash": "def456",
|
||||
"build_date": "2023-11-01T00:00:00.000Z",
|
||||
"build_snapshot": false,
|
||||
"lucene_version": "9.8.0"
|
||||
}
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, clusterInfo)
|
||||
assert.Equal(t, "serverless", clusterInfo.Version.BuildFlavor)
|
||||
assert.True(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should return error when HTTP request fails", func(t *testing.T) {
|
||||
clusterInfo, err := GetClusterInfo(http.DefaultClient, "http://invalid-url-that-does-not-exist.local:9999")
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ClusterInfo{}, clusterInfo)
|
||||
assert.Contains(t, err.Error(), "error getting ES cluster info")
|
||||
})
|
||||
|
||||
t.Run("Should return error when response body is invalid JSON", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
_, err := rw.Write([]byte(`{"invalid json`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ClusterInfo{}, clusterInfo)
|
||||
assert.Contains(t, err.Error(), "error decoding ES cluster info")
|
||||
})
|
||||
|
||||
t.Run("Should handle empty version object", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
_, err := rw.Write([]byte(`{
|
||||
"name": "test-cluster",
|
||||
"version": {}
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ClusterInfo{}, clusterInfo)
|
||||
assert.Equal(t, "", clusterInfo.Version.BuildFlavor)
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should handle HTTP error status codes", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
_, err := rw.Write([]byte(`{"error": "Unauthorized"}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ClusterInfo{}, clusterInfo)
|
||||
assert.Contains(t, err.Error(), "unexpected status code 401 getting ES cluster info")
|
||||
})
|
||||
}
|
||||
|
||||
func TestClusterInfo_IsServerless(t *testing.T) {
|
||||
t.Run("Should return true when build_flavor is serverless", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{
|
||||
Version: VersionInfo{
|
||||
BuildFlavor: BuildFlavorServerless,
|
||||
},
|
||||
}
|
||||
|
||||
assert.True(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should return false when build_flavor is default", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{
|
||||
Version: VersionInfo{
|
||||
BuildFlavor: "default",
|
||||
},
|
||||
}
|
||||
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should return false when build_flavor is empty", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{
|
||||
Version: VersionInfo{
|
||||
BuildFlavor: "",
|
||||
},
|
||||
}
|
||||
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should return false when build_flavor is unknown value", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{
|
||||
Version: VersionInfo{
|
||||
BuildFlavor: "unknown",
|
||||
},
|
||||
}
|
||||
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("should return false when cluster info is empty", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{}
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
func TestSearchRequest(t *testing.T) {
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
// processQuery processes a single query and adds it to the multi-search request builder
|
||||
|
||||
@@ -3,7 +3,7 @@ package elasticsearch
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
// setFloatPath converts a string value at the specified path to float64
|
||||
|
||||
@@ -88,14 +88,6 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins
|
||||
httpCliOpts.SigV4.Service = "es"
|
||||
}
|
||||
|
||||
apiKeyAuth, ok := jsonData["apiKeyAuth"].(bool)
|
||||
if ok && apiKeyAuth {
|
||||
apiKey := settings.DecryptedSecureJSONData["apiKey"]
|
||||
if apiKey != "" {
|
||||
httpCliOpts.Header.Add("Authorization", "ApiKey "+apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
httpCli, err := httpClientProvider.New(httpCliOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -159,11 +151,6 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins
|
||||
includeFrozen = false
|
||||
}
|
||||
|
||||
clusterInfo, err := es.GetClusterInfo(httpCli, settings.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configuredFields := es.ConfiguredFields{
|
||||
TimeField: timeField,
|
||||
LogLevelField: logLevelField,
|
||||
@@ -179,7 +166,6 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins
|
||||
ConfiguredFields: configuredFields,
|
||||
Interval: interval,
|
||||
IncludeFrozen: includeFrozen,
|
||||
ClusterInfo: clusterInfo,
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ package elasticsearch
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
@@ -20,26 +18,8 @@ type datasourceInfo struct {
|
||||
Interval string `json:"interval"`
|
||||
}
|
||||
|
||||
// mockElasticsearchServer creates a test HTTP server that mocks Elasticsearch cluster info endpoint
|
||||
func mockElasticsearchServer() *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// Return a mock Elasticsearch cluster info response
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"version": map[string]interface{}{
|
||||
"build_flavor": "serverless",
|
||||
"number": "8.0.0",
|
||||
},
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
func TestNewInstanceSettings(t *testing.T) {
|
||||
t.Run("fields exist", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: 5,
|
||||
@@ -48,7 +28,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -58,9 +37,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
|
||||
t.Run("timeField", func(t *testing.T) {
|
||||
t.Run("is nil", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
MaxConcurrentShardRequests: 5,
|
||||
Interval: "Daily",
|
||||
@@ -70,7 +46,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -79,9 +54,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("is empty", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
MaxConcurrentShardRequests: 5,
|
||||
Interval: "Daily",
|
||||
@@ -92,7 +64,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -103,9 +74,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
|
||||
t.Run("maxConcurrentShardRequests", func(t *testing.T) {
|
||||
t.Run("no maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
}
|
||||
@@ -113,7 +81,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -123,9 +90,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("string maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: "10",
|
||||
@@ -134,7 +98,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -144,9 +107,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("number maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: 10,
|
||||
@@ -155,7 +115,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -165,9 +124,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("zero maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: 0,
|
||||
@@ -176,7 +132,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -186,9 +141,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("negative maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: -10,
|
||||
@@ -197,7 +149,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -207,9 +158,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("float maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: 10.5,
|
||||
@@ -218,7 +166,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -228,9 +175,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("invalid maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: "invalid",
|
||||
@@ -239,7 +183,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
|
||||
Message: "Health check failed: Failed to get data source info",
|
||||
}, nil
|
||||
}
|
||||
|
||||
healthStatusUrl, err := url.Parse(ds.URL)
|
||||
if err != nil {
|
||||
logger.Error("Failed to parse data source URL", "error", err)
|
||||
@@ -37,14 +38,6 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
|
||||
}, nil
|
||||
}
|
||||
|
||||
// If the cluster is serverless, return a healthy result
|
||||
if ds.ClusterInfo.IsServerless() {
|
||||
return &backend.CheckHealthResult{
|
||||
Status: backend.HealthStatusOk,
|
||||
Message: "Elasticsearch Serverless data source is healthy.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// check that ES is healthy
|
||||
healthStatusUrl.Path = path.Join(healthStatusUrl.Path, "_cluster/health")
|
||||
healthStatusUrl.RawQuery = "wait_for_status=yellow"
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
// metricsResponseProcessor handles processing of metrics query responses
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
// Query represents the time series query model of the datasource
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
func parseQuery(tsdbQuery []backend.DataQuery, logger log.Logger) ([]*Query, error) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
// AggregationParser parses raw Elasticsearch DSL aggregations
|
||||
|
||||
@@ -15,9 +15,9 @@ import (
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/instrumentation"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
// flatten flattens multi-level objects to single level objects. It uses dot notation to join keys.
|
||||
|
||||
@@ -1,582 +0,0 @@
|
||||
// Package simplejson provides a wrapper for arbitrary JSON objects that adds methods to access properties.
|
||||
// Use of this package in place of types and the standard library's encoding/json package is strongly discouraged.
|
||||
//
|
||||
// Don't lint for stale code, since it's a copied library and we might as well keep the whole thing.
|
||||
// nolint:unused
|
||||
package simplejson
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// returns the current implementation version
|
||||
func Version() string {
|
||||
return "0.5.0"
|
||||
}
|
||||
|
||||
type Json struct {
|
||||
data any
|
||||
}
|
||||
|
||||
func (j *Json) FromDB(data []byte) error {
|
||||
j.data = make(map[string]any)
|
||||
|
||||
dec := json.NewDecoder(bytes.NewBuffer(data))
|
||||
dec.UseNumber()
|
||||
return dec.Decode(&j.data)
|
||||
}
|
||||
|
||||
func (j *Json) ToDB() ([]byte, error) {
|
||||
if j == nil || j.data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return j.Encode()
|
||||
}
|
||||
|
||||
func (j *Json) Scan(val any) error {
|
||||
switch v := val.(type) {
|
||||
case []byte:
|
||||
if len(v) == 0 {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(v, &j)
|
||||
case string:
|
||||
if len(v) == 0 {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal([]byte(v), &j)
|
||||
default:
|
||||
return fmt.Errorf("unsupported type: %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
func (j *Json) Value() (driver.Value, error) {
|
||||
return j.ToDB()
|
||||
}
|
||||
|
||||
// DeepCopyInto creates a copy by serializing JSON
|
||||
func (j *Json) DeepCopyInto(out *Json) {
|
||||
b, err := j.Encode()
|
||||
if err == nil {
|
||||
_ = out.UnmarshalJSON(b)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy will make a deep copy of the JSON object
|
||||
func (j *Json) DeepCopy() *Json {
|
||||
if j == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Json)
|
||||
j.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// NewJson returns a pointer to a new `Json` object
|
||||
// after unmarshaling `body` bytes
|
||||
func NewJson(body []byte) (*Json, error) {
|
||||
j := new(Json)
|
||||
err := j.UnmarshalJSON(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return j, nil
|
||||
}
|
||||
|
||||
// MustJson returns a pointer to a new `Json` object, panicking if `body` cannot be parsed.
|
||||
func MustJson(body []byte) *Json {
|
||||
j, err := NewJson(body)
|
||||
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("could not unmarshal JSON: %q", err))
|
||||
}
|
||||
|
||||
return j
|
||||
}
|
||||
|
||||
// New returns a pointer to a new, empty `Json` object
|
||||
func New() *Json {
|
||||
return &Json{
|
||||
data: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
// NewFromAny returns a pointer to a new `Json` object with provided data.
|
||||
func NewFromAny(data any) *Json {
|
||||
return &Json{data: data}
|
||||
}
|
||||
|
||||
// Interface returns the underlying data
|
||||
func (j *Json) Interface() any {
|
||||
return j.data
|
||||
}
|
||||
|
||||
// Encode returns its marshaled data as `[]byte`
|
||||
func (j *Json) Encode() ([]byte, error) {
|
||||
return j.MarshalJSON()
|
||||
}
|
||||
|
||||
// EncodePretty returns its marshaled data as `[]byte` with indentation
|
||||
func (j *Json) EncodePretty() ([]byte, error) {
|
||||
return json.MarshalIndent(&j.data, "", " ")
|
||||
}
|
||||
|
||||
// Implements the json.Marshaler interface.
|
||||
func (j *Json) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&j.data)
|
||||
}
|
||||
|
||||
// Set modifies `Json` map by `key` and `value`
|
||||
// Useful for changing single key/value in a `Json` object easily.
|
||||
func (j *Json) Set(key string, val any) {
|
||||
m, err := j.Map()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
m[key] = val
|
||||
}
|
||||
|
||||
// SetPath modifies `Json`, recursively checking/creating map keys for the supplied path,
|
||||
// and then finally writing in the value
|
||||
func (j *Json) SetPath(branch []string, val any) {
|
||||
if len(branch) == 0 {
|
||||
j.data = val
|
||||
return
|
||||
}
|
||||
|
||||
// in order to insert our branch, we need map[string]any
|
||||
if _, ok := (j.data).(map[string]any); !ok {
|
||||
// have to replace with something suitable
|
||||
j.data = make(map[string]any)
|
||||
}
|
||||
curr := j.data.(map[string]any)
|
||||
|
||||
for i := 0; i < len(branch)-1; i++ {
|
||||
b := branch[i]
|
||||
// key exists?
|
||||
if _, ok := curr[b]; !ok {
|
||||
n := make(map[string]any)
|
||||
curr[b] = n
|
||||
curr = n
|
||||
continue
|
||||
}
|
||||
|
||||
// make sure the value is the right sort of thing
|
||||
if _, ok := curr[b].(map[string]any); !ok {
|
||||
// have to replace with something suitable
|
||||
n := make(map[string]any)
|
||||
curr[b] = n
|
||||
}
|
||||
|
||||
curr = curr[b].(map[string]any)
|
||||
}
|
||||
|
||||
// add remaining k/v
|
||||
curr[branch[len(branch)-1]] = val
|
||||
}
|
||||
|
||||
// Del modifies `Json` map by deleting `key` if it is present.
|
||||
func (j *Json) Del(key string) {
|
||||
m, err := j.Map()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
delete(m, key)
|
||||
}
|
||||
|
||||
// Get returns a pointer to a new `Json` object
|
||||
// for `key` in its `map` representation
|
||||
//
|
||||
// useful for chaining operations (to traverse a nested JSON):
|
||||
//
|
||||
// js.Get("top_level").Get("dict").Get("value").Int()
|
||||
func (j *Json) Get(key string) *Json {
|
||||
m, err := j.Map()
|
||||
if err == nil {
|
||||
if val, ok := m[key]; ok {
|
||||
return &Json{val}
|
||||
}
|
||||
}
|
||||
return &Json{nil}
|
||||
}
|
||||
|
||||
// GetPath searches for the item as specified by the branch
|
||||
// without the need to deep dive using Get()'s.
|
||||
//
|
||||
// js.GetPath("top_level", "dict")
|
||||
func (j *Json) GetPath(branch ...string) *Json {
|
||||
jin := j
|
||||
for _, p := range branch {
|
||||
jin = jin.Get(p)
|
||||
}
|
||||
return jin
|
||||
}
|
||||
|
||||
// GetIndex returns a pointer to a new `Json` object
|
||||
// for `index` in its `array` representation
|
||||
//
|
||||
// this is the analog to Get when accessing elements of
|
||||
// a json array instead of a json object:
|
||||
//
|
||||
// js.Get("top_level").Get("array").GetIndex(1).Get("key").Int()
|
||||
func (j *Json) GetIndex(index int) *Json {
|
||||
a, err := j.Array()
|
||||
if err == nil {
|
||||
if len(a) > index {
|
||||
return &Json{a[index]}
|
||||
}
|
||||
}
|
||||
return &Json{nil}
|
||||
}
|
||||
|
||||
// CheckGetIndex returns a pointer to a new `Json` object
|
||||
// for `index` in its `array` representation, and a `bool`
|
||||
// indicating success or failure
|
||||
//
|
||||
// useful for chained operations when success is important:
|
||||
//
|
||||
// if data, ok := js.Get("top_level").CheckGetIndex(0); ok {
|
||||
// log.Println(data)
|
||||
// }
|
||||
func (j *Json) CheckGetIndex(index int) (*Json, bool) {
|
||||
a, err := j.Array()
|
||||
if err == nil {
|
||||
if len(a) > index {
|
||||
return &Json{a[index]}, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// SetIndex modifies `Json` array by `index` and `value`
|
||||
// for `index` in its `array` representation
|
||||
func (j *Json) SetIndex(index int, val any) {
|
||||
a, err := j.Array()
|
||||
if err == nil {
|
||||
if len(a) > index {
|
||||
a[index] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckGet returns a pointer to a new `Json` object and
|
||||
// a `bool` identifying success or failure
|
||||
//
|
||||
// useful for chained operations when success is important:
|
||||
//
|
||||
// if data, ok := js.Get("top_level").CheckGet("inner"); ok {
|
||||
// log.Println(data)
|
||||
// }
|
||||
func (j *Json) CheckGet(key string) (*Json, bool) {
|
||||
m, err := j.Map()
|
||||
if err == nil {
|
||||
if val, ok := m[key]; ok {
|
||||
return &Json{val}, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Map type asserts to `map`
|
||||
func (j *Json) Map() (map[string]any, error) {
|
||||
if m, ok := (j.data).(map[string]any); ok {
|
||||
return m, nil
|
||||
}
|
||||
return nil, errors.New("type assertion to map[string]any failed")
|
||||
}
|
||||
|
||||
// Array type asserts to an `array`
|
||||
func (j *Json) Array() ([]any, error) {
|
||||
if a, ok := (j.data).([]any); ok {
|
||||
return a, nil
|
||||
}
|
||||
return nil, errors.New("type assertion to []any failed")
|
||||
}
|
||||
|
||||
// Bool type asserts to `bool`
|
||||
func (j *Json) Bool() (bool, error) {
|
||||
if s, ok := (j.data).(bool); ok {
|
||||
return s, nil
|
||||
}
|
||||
return false, errors.New("type assertion to bool failed")
|
||||
}
|
||||
|
||||
// String type asserts to `string`
|
||||
func (j *Json) String() (string, error) {
|
||||
if s, ok := (j.data).(string); ok {
|
||||
return s, nil
|
||||
}
|
||||
return "", errors.New("type assertion to string failed")
|
||||
}
|
||||
|
||||
// Bytes type asserts to `[]byte`
|
||||
func (j *Json) Bytes() ([]byte, error) {
|
||||
if s, ok := (j.data).(string); ok {
|
||||
return []byte(s), nil
|
||||
}
|
||||
return nil, errors.New("type assertion to []byte failed")
|
||||
}
|
||||
|
||||
// StringArray type asserts to an `array` of `string`
|
||||
func (j *Json) StringArray() ([]string, error) {
|
||||
arr, err := j.Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
retArr := make([]string, 0, len(arr))
|
||||
for _, a := range arr {
|
||||
if a == nil {
|
||||
retArr = append(retArr, "")
|
||||
continue
|
||||
}
|
||||
s, ok := a.(string)
|
||||
if !ok {
|
||||
return nil, err
|
||||
}
|
||||
retArr = append(retArr, s)
|
||||
}
|
||||
return retArr, nil
|
||||
}
|
||||
|
||||
// MustArray guarantees the return of a `[]any` (with optional default)
|
||||
//
|
||||
// useful when you want to iterate over array values in a succinct manner:
|
||||
//
|
||||
// for i, v := range js.Get("results").MustArray() {
|
||||
// fmt.Println(i, v)
|
||||
// }
|
||||
func (j *Json) MustArray(args ...[]any) []any {
|
||||
var def []any
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustArray() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
a, err := j.Array()
|
||||
if err == nil {
|
||||
return a
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MustMap guarantees the return of a `map[string]any` (with optional default)
|
||||
//
|
||||
// useful when you want to iterate over map values in a succinct manner:
|
||||
//
|
||||
// for k, v := range js.Get("dictionary").MustMap() {
|
||||
// fmt.Println(k, v)
|
||||
// }
|
||||
func (j *Json) MustMap(args ...map[string]any) map[string]any {
|
||||
var def map[string]any
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustMap() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
a, err := j.Map()
|
||||
if err == nil {
|
||||
return a
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MustString guarantees the return of a `string` (with optional default)
|
||||
//
|
||||
// useful when you explicitly want a `string` in a single value return context:
|
||||
//
|
||||
// myFunc(js.Get("param1").MustString(), js.Get("optional_param").MustString("my_default"))
|
||||
func (j *Json) MustString(args ...string) string {
|
||||
var def string
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustString() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
s, err := j.String()
|
||||
if err == nil {
|
||||
return s
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MustStringArray guarantees the return of a `[]string` (with optional default)
|
||||
//
|
||||
// useful when you want to iterate over array values in a succinct manner:
|
||||
//
|
||||
// for i, s := range js.Get("results").MustStringArray() {
|
||||
// fmt.Println(i, s)
|
||||
// }
|
||||
func (j *Json) MustStringArray(args ...[]string) []string {
|
||||
var def []string
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustStringArray() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
a, err := j.StringArray()
|
||||
if err == nil {
|
||||
return a
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MustInt guarantees the return of an `int` (with optional default)
|
||||
//
|
||||
// useful when you explicitly want an `int` in a single value return context:
|
||||
//
|
||||
// myFunc(js.Get("param1").MustInt(), js.Get("optional_param").MustInt(5150))
|
||||
func (j *Json) MustInt(args ...int) int {
|
||||
var def int
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustInt() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
i, err := j.Int()
|
||||
if err == nil {
|
||||
return i
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MustFloat64 guarantees the return of a `float64` (with optional default)
|
||||
//
|
||||
// useful when you explicitly want a `float64` in a single value return context:
|
||||
//
|
||||
// myFunc(js.Get("param1").MustFloat64(), js.Get("optional_param").MustFloat64(5.150))
|
||||
func (j *Json) MustFloat64(args ...float64) float64 {
|
||||
var def float64
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustFloat64() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
f, err := j.Float64()
|
||||
if err == nil {
|
||||
return f
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MustBool guarantees the return of a `bool` (with optional default)
|
||||
//
|
||||
// useful when you explicitly want a `bool` in a single value return context:
|
||||
//
|
||||
// myFunc(js.Get("param1").MustBool(), js.Get("optional_param").MustBool(true))
|
||||
func (j *Json) MustBool(args ...bool) bool {
|
||||
var def bool
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustBool() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
b, err := j.Bool()
|
||||
if err == nil {
|
||||
return b
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MustInt64 guarantees the return of an `int64` (with optional default)
|
||||
//
|
||||
// useful when you explicitly want an `int64` in a single value return context:
|
||||
//
|
||||
// myFunc(js.Get("param1").MustInt64(), js.Get("optional_param").MustInt64(5150))
|
||||
func (j *Json) MustInt64(args ...int64) int64 {
|
||||
var def int64
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustInt64() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
i, err := j.Int64()
|
||||
if err == nil {
|
||||
return i
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MustUInt64 guarantees the return of an `uint64` (with optional default)
|
||||
//
|
||||
// useful when you explicitly want an `uint64` in a single value return context:
|
||||
//
|
||||
// myFunc(js.Get("param1").MustUint64(), js.Get("optional_param").MustUint64(5150))
|
||||
func (j *Json) MustUint64(args ...uint64) uint64 {
|
||||
var def uint64
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustUint64() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
i, err := j.Uint64()
|
||||
if err == nil {
|
||||
return i
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MarshalYAML implements yaml.Marshaller.
|
||||
func (j *Json) MarshalYAML() (any, error) {
|
||||
return j.data, nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaller.
|
||||
func (j *Json) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var data any
|
||||
if err := unmarshal(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
j.data = data
|
||||
return nil
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package simplejson
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Implements the json.Unmarshaler interface.
|
||||
func (j *Json) UnmarshalJSON(p []byte) error {
|
||||
dec := json.NewDecoder(bytes.NewBuffer(p))
|
||||
dec.UseNumber()
|
||||
return dec.Decode(&j.data)
|
||||
}
|
||||
|
||||
// NewFromReader returns a *Json by decoding from an io.Reader
|
||||
func NewFromReader(r io.Reader) (*Json, error) {
|
||||
j := new(Json)
|
||||
dec := json.NewDecoder(r)
|
||||
dec.UseNumber()
|
||||
err := dec.Decode(&j.data)
|
||||
return j, err
|
||||
}
|
||||
|
||||
// Float64 coerces into a float64
|
||||
func (j *Json) Float64() (float64, error) {
|
||||
switch n := j.data.(type) {
|
||||
case json.Number:
|
||||
return n.Float64()
|
||||
case float32, float64:
|
||||
return reflect.ValueOf(j.data).Float(), nil
|
||||
case int, int8, int16, int32, int64:
|
||||
return float64(reflect.ValueOf(j.data).Int()), nil
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return float64(reflect.ValueOf(j.data).Uint()), nil
|
||||
}
|
||||
return 0, errors.New("invalid value type")
|
||||
}
|
||||
|
||||
// Int coerces into an int
|
||||
func (j *Json) Int() (int, error) {
|
||||
switch n := j.data.(type) {
|
||||
case json.Number:
|
||||
i, err := n.Int64()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(i), nil
|
||||
case float32, float64:
|
||||
return int(reflect.ValueOf(j.data).Float()), nil
|
||||
case int, int8, int16, int32, int64:
|
||||
return int(reflect.ValueOf(j.data).Int()), nil
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return int(reflect.ValueOf(j.data).Uint()), nil
|
||||
}
|
||||
return 0, errors.New("invalid value type")
|
||||
}
|
||||
|
||||
// Int64 coerces into an int64
|
||||
func (j *Json) Int64() (int64, error) {
|
||||
switch n := j.data.(type) {
|
||||
case json.Number:
|
||||
return n.Int64()
|
||||
case float32, float64:
|
||||
return int64(reflect.ValueOf(j.data).Float()), nil
|
||||
case int, int8, int16, int32, int64:
|
||||
return reflect.ValueOf(j.data).Int(), nil
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return int64(reflect.ValueOf(j.data).Uint()), nil
|
||||
}
|
||||
return 0, errors.New("invalid value type")
|
||||
}
|
||||
|
||||
// Uint64 coerces into an uint64
|
||||
func (j *Json) Uint64() (uint64, error) {
|
||||
switch n := j.data.(type) {
|
||||
case json.Number:
|
||||
return strconv.ParseUint(n.String(), 10, 64)
|
||||
case float32, float64:
|
||||
return uint64(reflect.ValueOf(j.data).Float()), nil
|
||||
case int, int8, int16, int32, int64:
|
||||
return uint64(reflect.ValueOf(j.data).Int()), nil
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return reflect.ValueOf(j.data).Uint(), nil
|
||||
}
|
||||
return 0, errors.New("invalid value type")
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
package simplejson
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSimplejson(t *testing.T) {
|
||||
var ok bool
|
||||
var err error
|
||||
|
||||
js, err := NewJson([]byte(`{
|
||||
"test": {
|
||||
"string_array": ["asdf", "ghjk", "zxcv"],
|
||||
"string_array_null": ["abc", null, "efg"],
|
||||
"array": [1, "2", 3],
|
||||
"arraywithsubs": [{"subkeyone": 1},
|
||||
{"subkeytwo": 2, "subkeythree": 3}],
|
||||
"int": 10,
|
||||
"float": 5.150,
|
||||
"string": "simplejson",
|
||||
"bool": true,
|
||||
"sub_obj": {"a": 1}
|
||||
}
|
||||
}`))
|
||||
|
||||
assert.NotEqual(t, nil, js)
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
_, ok = js.CheckGet("test")
|
||||
assert.Equal(t, true, ok)
|
||||
|
||||
_, ok = js.CheckGet("missing_key")
|
||||
assert.Equal(t, false, ok)
|
||||
|
||||
aws := js.Get("test").Get("arraywithsubs")
|
||||
assert.NotEqual(t, nil, aws)
|
||||
var awsval int
|
||||
awsval, _ = aws.GetIndex(0).Get("subkeyone").Int()
|
||||
assert.Equal(t, 1, awsval)
|
||||
awsval, _ = aws.GetIndex(1).Get("subkeytwo").Int()
|
||||
assert.Equal(t, 2, awsval)
|
||||
awsval, _ = aws.GetIndex(1).Get("subkeythree").Int()
|
||||
assert.Equal(t, 3, awsval)
|
||||
|
||||
arr := js.Get("test").Get("array")
|
||||
assert.NotEqual(t, nil, arr)
|
||||
val, ok := arr.CheckGetIndex(0)
|
||||
assert.Equal(t, ok, true)
|
||||
valInt, _ := val.Int()
|
||||
assert.Equal(t, valInt, 1)
|
||||
val, ok = arr.CheckGetIndex(1)
|
||||
assert.Equal(t, ok, true)
|
||||
valStr, _ := val.String()
|
||||
assert.Equal(t, valStr, "2")
|
||||
val, ok = arr.CheckGetIndex(2)
|
||||
assert.Equal(t, ok, true)
|
||||
valInt, _ = val.Int()
|
||||
assert.Equal(t, valInt, 3)
|
||||
_, ok = arr.CheckGetIndex(3)
|
||||
assert.Equal(t, ok, false)
|
||||
|
||||
i, _ := js.Get("test").Get("int").Int()
|
||||
assert.Equal(t, 10, i)
|
||||
|
||||
f, _ := js.Get("test").Get("float").Float64()
|
||||
assert.Equal(t, 5.150, f)
|
||||
|
||||
s, _ := js.Get("test").Get("string").String()
|
||||
assert.Equal(t, "simplejson", s)
|
||||
|
||||
b, _ := js.Get("test").Get("bool").Bool()
|
||||
assert.Equal(t, true, b)
|
||||
|
||||
mi := js.Get("test").Get("int").MustInt()
|
||||
assert.Equal(t, 10, mi)
|
||||
|
||||
mi2 := js.Get("test").Get("missing_int").MustInt(5150)
|
||||
assert.Equal(t, 5150, mi2)
|
||||
|
||||
ms := js.Get("test").Get("string").MustString()
|
||||
assert.Equal(t, "simplejson", ms)
|
||||
|
||||
ms2 := js.Get("test").Get("missing_string").MustString("fyea")
|
||||
assert.Equal(t, "fyea", ms2)
|
||||
|
||||
ma2 := js.Get("test").Get("missing_array").MustArray([]any{"1", 2, "3"})
|
||||
assert.Equal(t, ma2, []any{"1", 2, "3"})
|
||||
|
||||
msa := js.Get("test").Get("string_array").MustStringArray()
|
||||
assert.Equal(t, msa[0], "asdf")
|
||||
assert.Equal(t, msa[1], "ghjk")
|
||||
assert.Equal(t, msa[2], "zxcv")
|
||||
|
||||
msa2 := js.Get("test").Get("string_array").MustStringArray([]string{"1", "2", "3"})
|
||||
assert.Equal(t, msa2[0], "asdf")
|
||||
assert.Equal(t, msa2[1], "ghjk")
|
||||
assert.Equal(t, msa2[2], "zxcv")
|
||||
|
||||
msa3 := js.Get("test").Get("missing_array").MustStringArray([]string{"1", "2", "3"})
|
||||
assert.Equal(t, msa3, []string{"1", "2", "3"})
|
||||
|
||||
mm2 := js.Get("test").Get("missing_map").MustMap(map[string]any{"found": false})
|
||||
assert.Equal(t, mm2, map[string]any{"found": false})
|
||||
|
||||
strs, err := js.Get("test").Get("string_array").StringArray()
|
||||
assert.Equal(t, err, nil)
|
||||
assert.Equal(t, strs[0], "asdf")
|
||||
assert.Equal(t, strs[1], "ghjk")
|
||||
assert.Equal(t, strs[2], "zxcv")
|
||||
|
||||
strs2, err := js.Get("test").Get("string_array_null").StringArray()
|
||||
assert.Equal(t, err, nil)
|
||||
assert.Equal(t, strs2[0], "abc")
|
||||
assert.Equal(t, strs2[1], "")
|
||||
assert.Equal(t, strs2[2], "efg")
|
||||
|
||||
gp, _ := js.GetPath("test", "string").String()
|
||||
assert.Equal(t, "simplejson", gp)
|
||||
|
||||
gp2, _ := js.GetPath("test", "int").Int()
|
||||
assert.Equal(t, 10, gp2)
|
||||
|
||||
assert.Equal(t, js.Get("test").Get("bool").MustBool(), true)
|
||||
|
||||
js.Set("float2", 300.0)
|
||||
assert.Equal(t, js.Get("float2").MustFloat64(), 300.0)
|
||||
|
||||
js.Set("test2", "setTest")
|
||||
assert.Equal(t, "setTest", js.Get("test2").MustString())
|
||||
|
||||
js.Del("test2")
|
||||
assert.NotEqual(t, "setTest", js.Get("test2").MustString())
|
||||
|
||||
js.Get("test").Get("sub_obj").Set("a", 2)
|
||||
assert.Equal(t, 2, js.Get("test").Get("sub_obj").Get("a").MustInt())
|
||||
|
||||
js.GetPath("test", "sub_obj").Set("a", 3)
|
||||
assert.Equal(t, 3, js.GetPath("test", "sub_obj", "a").MustInt())
|
||||
}
|
||||
|
||||
func TestStdlibInterfaces(t *testing.T) {
|
||||
val := new(struct {
|
||||
Name string `json:"name"`
|
||||
Params *Json `json:"params"`
|
||||
})
|
||||
val2 := new(struct {
|
||||
Name string `json:"name"`
|
||||
Params *Json `json:"params"`
|
||||
})
|
||||
|
||||
raw := `{"name":"myobject","params":{"string":"simplejson"}}`
|
||||
|
||||
assert.Equal(t, nil, json.Unmarshal([]byte(raw), val))
|
||||
|
||||
assert.Equal(t, "myobject", val.Name)
|
||||
assert.NotEqual(t, nil, val.Params.data)
|
||||
s, _ := val.Params.Get("string").String()
|
||||
assert.Equal(t, "simplejson", s)
|
||||
|
||||
p, err := json.Marshal(val)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, nil, json.Unmarshal(p, val2))
|
||||
assert.Equal(t, val, val2) // stable
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
js, err := NewJson([]byte(`{}`))
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
js.Set("baz", "bing")
|
||||
|
||||
s, err := js.GetPath("baz").String()
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "bing", s)
|
||||
}
|
||||
|
||||
func TestReplace(t *testing.T) {
|
||||
js, err := NewJson([]byte(`{}`))
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
err = js.UnmarshalJSON([]byte(`{"baz":"bing"}`))
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
s, err := js.GetPath("baz").String()
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "bing", s)
|
||||
}
|
||||
|
||||
func TestSetPath(t *testing.T) {
|
||||
js, err := NewJson([]byte(`{}`))
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
js.SetPath([]string{"foo", "bar"}, "baz")
|
||||
|
||||
s, err := js.GetPath("foo", "bar").String()
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "baz", s)
|
||||
}
|
||||
|
||||
func TestSetPathNoPath(t *testing.T) {
|
||||
js, err := NewJson([]byte(`{"some":"data","some_number":1.0,"some_bool":false}`))
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
f := js.GetPath("some_number").MustFloat64(99.0)
|
||||
assert.Equal(t, f, 1.0)
|
||||
|
||||
js.SetPath([]string{}, map[string]any{"foo": "bar"})
|
||||
|
||||
s, err := js.GetPath("foo").String()
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "bar", s)
|
||||
|
||||
f = js.GetPath("some_number").MustFloat64(99.0)
|
||||
assert.Equal(t, f, 99.0)
|
||||
}
|
||||
|
||||
func TestPathWillAugmentExisting(t *testing.T) {
|
||||
js, err := NewJson([]byte(`{"this":{"a":"aa","b":"bb","c":"cc"}}`))
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
js.SetPath([]string{"this", "d"}, "dd")
|
||||
|
||||
cases := []struct {
|
||||
path []string
|
||||
outcome string
|
||||
}{
|
||||
{
|
||||
path: []string{"this", "a"},
|
||||
outcome: "aa",
|
||||
},
|
||||
{
|
||||
path: []string{"this", "b"},
|
||||
outcome: "bb",
|
||||
},
|
||||
{
|
||||
path: []string{"this", "c"},
|
||||
outcome: "cc",
|
||||
},
|
||||
{
|
||||
path: []string{"this", "d"},
|
||||
outcome: "dd",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
s, err := js.GetPath(tc.path...).String()
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, tc.outcome, s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathWillOverwriteExisting(t *testing.T) {
|
||||
// notice how "a" is 0.1 - but then we'll try to set at path a, foo
|
||||
js, err := NewJson([]byte(`{"this":{"a":0.1,"b":"bb","c":"cc"}}`))
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
js.SetPath([]string{"this", "a", "foo"}, "bar")
|
||||
|
||||
s, err := js.GetPath("this", "a", "foo").String()
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "bar", s)
|
||||
}
|
||||
|
||||
func TestMustJson(t *testing.T) {
|
||||
js := MustJson([]byte(`{"foo": "bar"}`))
|
||||
assert.Equal(t, js.Get("foo").MustString(), "bar")
|
||||
|
||||
assert.PanicsWithValue(t, "could not unmarshal JSON: \"unexpected EOF\"", func() {
|
||||
MustJson([]byte(`{`))
|
||||
})
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
elasticsearch "github.com/grafana/grafana/pkg/tsdb/elasticsearch"
|
||||
)
|
||||
|
||||
var (
|
||||
_ backend.QueryDataHandler = (*Datasource)(nil)
|
||||
_ backend.CheckHealthHandler = (*Datasource)(nil)
|
||||
_ backend.CallResourceHandler = (*Datasource)(nil)
|
||||
)
|
||||
|
||||
func NewDatasource(context.Context, backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return &Datasource{
|
||||
Service: elasticsearch.ProvideService(httpclient.NewProvider()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Datasource struct {
|
||||
Service *elasticsearch.Service
|
||||
}
|
||||
|
||||
func contextualMiddlewares(ctx context.Context) context.Context {
|
||||
cfg := backend.GrafanaConfigFromContext(ctx)
|
||||
responseLimitMiddleware := httpclient.ResponseLimitMiddleware(cfg.ResponseLimit())
|
||||
ctx = httpclient.WithContextualMiddleware(ctx, responseLimitMiddleware)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
ctx = contextualMiddlewares(ctx)
|
||||
return d.Service.QueryData(ctx, req)
|
||||
}
|
||||
|
||||
func (d *Datasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
ctx = contextualMiddlewares(ctx)
|
||||
return d.Service.CallResource(ctx, req, sender)
|
||||
}
|
||||
|
||||
func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
ctx = contextualMiddlewares(ctx)
|
||||
return d.Service.CheckHealth(ctx, req)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Start listening to requests sent from Grafana. This call is blocking so
|
||||
// it won't finish until Grafana shuts down the process or the plugin choose
|
||||
// to exit by itself using os.Exit. Manage automatically manages life cycle
|
||||
// of datasource instances. It accepts datasource instance factory as first
|
||||
// argument. This factory will be automatically called on incoming request
|
||||
// from Grafana to create different instances of SampleDatasource (per datasource
|
||||
// ID). When datasource configuration changed Dispose method will be called and
|
||||
// new datasource instance created using NewSampleDatasource factory.
|
||||
if err := datasource.Manage("elasticsearch", NewDatasource, datasource.ManageOpts{}); err != nil {
|
||||
log.DefaultLogger.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { render, screen } from 'test/test-utils';
|
||||
|
||||
import { KnownProvenance } from '../types/knownProvenance';
|
||||
|
||||
import { ProvisioningBadge } from './Provisioning';
|
||||
|
||||
describe('ProvisioningBadge', () => {
|
||||
describe('when the provenance is file', () => {
|
||||
it('should render the badge with the correct text', () => {
|
||||
render(<ProvisioningBadge provenance={KnownProvenance.File} />);
|
||||
|
||||
expect(screen.getByText('Provisioned')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Imported')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render correct tooltip text', async () => {
|
||||
const { user } = render(<ProvisioningBadge tooltip provenance={KnownProvenance.File} />);
|
||||
|
||||
const badge = screen.getByText('Provisioned');
|
||||
await user.hover(badge);
|
||||
|
||||
expect(
|
||||
screen.getByText('This resource has been provisioned via file and cannot be edited through the UI')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the provenance is ConvertedPrometheus', () => {
|
||||
it('should render the badge with the correct text', () => {
|
||||
render(<ProvisioningBadge provenance={KnownProvenance.ConvertedPrometheus} />);
|
||||
|
||||
expect(screen.getByText('Imported')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Provisioned')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render correct tooltip text', async () => {
|
||||
const { user } = render(<ProvisioningBadge tooltip provenance={KnownProvenance.ConvertedPrometheus} />);
|
||||
|
||||
const badge = screen.getByText('Imported');
|
||||
await user.hover(badge);
|
||||
|
||||
expect(
|
||||
screen.getByText('This resource has been provisioned via Prometheus/Mimir and cannot be edited through the UI')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the provenance is API', () => {
|
||||
it('should render the badge with the correct text', () => {
|
||||
render(<ProvisioningBadge provenance={KnownProvenance.API} />);
|
||||
|
||||
expect(screen.getByText('Provisioned')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Imported')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render correct tooltip text', async () => {
|
||||
const { user } = render(<ProvisioningBadge tooltip provenance={KnownProvenance.API} />);
|
||||
|
||||
const badge = screen.getByText('Provisioned');
|
||||
await user.hover(badge);
|
||||
|
||||
expect(
|
||||
screen.getByText('This resource has been provisioned via api and cannot be edited through the UI')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,8 +3,6 @@ import { ComponentPropsWithoutRef } from 'react';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Alert, Badge, Tooltip } from '@grafana/ui';
|
||||
|
||||
import { KnownProvenance } from '../types/knownProvenance';
|
||||
|
||||
export enum ProvisionedResource {
|
||||
ContactPoint = 'contact point',
|
||||
Template = 'template',
|
||||
@@ -66,17 +64,11 @@ export const ProvisioningBadge = ({
|
||||
*/
|
||||
provenance?: string;
|
||||
}) => {
|
||||
const isConvertedPrometheus = provenance === KnownProvenance.ConvertedPrometheus;
|
||||
const badgeText = isConvertedPrometheus
|
||||
? t('alerting.provisioning-badge.badge.text-converted-prometheus', 'Imported')
|
||||
: t('alerting.provisioning-badge.badge.text-provisioned', 'Provisioned');
|
||||
const badgeColor = isConvertedPrometheus ? 'blue' : 'purple';
|
||||
const badge = <Badge text={badgeText} color={badgeColor} />;
|
||||
const badge = <Badge text={t('alerting.provisioning-badge.badge.text-provisioned', 'Provisioned')} color="purple" />;
|
||||
|
||||
if (tooltip) {
|
||||
const provenanceText = isConvertedPrometheus ? 'Prometheus/Mimir' : provenance;
|
||||
const provenanceTooltip = (
|
||||
<Trans i18nKey="alerting.provisioning.badge-tooltip-provenance" values={{ provenance: provenanceText }}>
|
||||
<Trans i18nKey="alerting.provisioning.badge-tooltip-provenance" values={{ provenance }}>
|
||||
This resource has been provisioned via {{ provenance }} and cannot be edited through the UI
|
||||
</Trans>
|
||||
);
|
||||
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
import { render, screen } from 'test/test-utils';
|
||||
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import { setupMswServer } from '../../mockApi';
|
||||
import { grantUserPermissions } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
|
||||
import { ContactPointHeader } from './ContactPointHeader';
|
||||
import { ContactPointWithMetadata } from './utils';
|
||||
|
||||
setupMswServer();
|
||||
|
||||
const renderWithProvider = (component: React.ReactElement, alertmanagerSourceName?: string) => {
|
||||
return render(
|
||||
<AlertmanagerProvider accessType="notification" alertmanagerSourceName={alertmanagerSourceName}>
|
||||
{component}
|
||||
</AlertmanagerProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('ContactPointHeader', () => {
|
||||
beforeEach(() => {
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsWrite,
|
||||
]);
|
||||
});
|
||||
|
||||
const mockContactPoint: ContactPointWithMetadata = {
|
||||
id: 'test-contact-point',
|
||||
name: 'Test Contact Point',
|
||||
provenance: KnownProvenance.API,
|
||||
policies: [],
|
||||
grafana_managed_receiver_configs: [],
|
||||
};
|
||||
|
||||
it('shows Provisioned badge when contact point has file provenance via K8s annotations', () => {
|
||||
const contactPointWithFile = {
|
||||
...mockContactPoint,
|
||||
provenance: KnownProvenance.File,
|
||||
};
|
||||
|
||||
renderWithProvider(<ContactPointHeader contactPoint={contactPointWithFile} onDelete={jest.fn()} />);
|
||||
|
||||
expect(screen.getByText('Provisioned')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows correct badge when contact point has converted_prometheus provenance', () => {
|
||||
const contactPointWithConvertedPrometheus = {
|
||||
...mockContactPoint,
|
||||
provenance: KnownProvenance.ConvertedPrometheus,
|
||||
};
|
||||
|
||||
renderWithProvider(<ContactPointHeader contactPoint={contactPointWithConvertedPrometheus} onDelete={jest.fn()} />);
|
||||
|
||||
expect(screen.getByText('Imported')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+8
-9
@@ -13,7 +13,6 @@ import {
|
||||
canDeleteEntity,
|
||||
canEditEntity,
|
||||
getAnnotation,
|
||||
isProvisionedResource,
|
||||
shouldUseK8sApi,
|
||||
} from 'app/features/alerting/unified/utils/k8s/utils';
|
||||
|
||||
@@ -32,15 +31,13 @@ interface ContactPointHeaderProps {
|
||||
}
|
||||
|
||||
export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeaderProps) => {
|
||||
const { name, id, provenance, policies = [] } = contactPoint;
|
||||
const { name, id, provisioned, policies = [] } = contactPoint;
|
||||
const styles = useStyles2(getStyles);
|
||||
const [showPermissionsDrawer, setShowPermissionsDrawer] = useState(false);
|
||||
const { selectedAlertmanager } = useAlertmanager();
|
||||
|
||||
const usingK8sApi = shouldUseK8sApi(selectedAlertmanager!);
|
||||
|
||||
const isProvisioned = isProvisionedResource(provenance);
|
||||
|
||||
const [exportSupported, exportAllowed] = useAlertmanagerAbility(AlertmanagerAction.ExportContactPoint);
|
||||
const [editSupported, editAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateContactPoint);
|
||||
const [deleteSupported, deleteAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateContactPoint);
|
||||
@@ -73,14 +70,14 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade
|
||||
/** Does the current user have permissions to edit the contact point? */
|
||||
const hasAbilityToEdit = usingK8sApi ? canEditEntity(contactPoint) : editAllowed;
|
||||
/** Can the contact point actually be edited via the UI? */
|
||||
const contactPointIsEditable = !isProvisioned;
|
||||
const contactPointIsEditable = !provisioned;
|
||||
/** Given the alertmanager, the user's permissions, and the state of the contact point - can it actually be edited? */
|
||||
const canEdit = editSupported && hasAbilityToEdit && contactPointIsEditable;
|
||||
|
||||
/** Does the current user have permissions to delete the contact point? */
|
||||
const hasAbilityToDelete = usingK8sApi ? canDeleteEntity(contactPoint) : deleteAllowed;
|
||||
/** Can the contact point actually be deleted, regardless of permissions? i.e. ensuring it isn't provisioned and isn't referenced elsewhere */
|
||||
const contactPointIsDeleteable = !isProvisioned && !numberOfPoliciesPreventingDeletion && !numberOfRules;
|
||||
const contactPointIsDeleteable = !provisioned && !numberOfPoliciesPreventingDeletion && !numberOfRules;
|
||||
/** Given the alertmanager, the user's permissions, and the state of the contact point - can it actually be deleted? */
|
||||
const canBeDeleted = deleteSupported && hasAbilityToDelete && contactPointIsDeleteable;
|
||||
|
||||
@@ -133,7 +130,7 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade
|
||||
|
||||
const reasonsDeleteIsDisabled = [
|
||||
!hasAbilityToDelete ? cannotDeleteNoPermissions : '',
|
||||
isProvisioned ? cannotDeleteProvisioned : '',
|
||||
provisioned ? cannotDeleteProvisioned : '',
|
||||
numberOfPoliciesPreventingDeletion > 0 ? cannotDeletePolicies : '',
|
||||
numberOfRules ? cannotDeleteRules : '',
|
||||
].filter(Boolean);
|
||||
@@ -212,13 +209,15 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade
|
||||
{referencedByRulesText}
|
||||
</TextLink>
|
||||
)}
|
||||
{isProvisioned && <ProvisioningBadge tooltip provenance={provenance} />}
|
||||
{provisioned && (
|
||||
<ProvisioningBadge tooltip provenance={getAnnotation(contactPoint, K8sAnnotations.Provenance)} />
|
||||
)}
|
||||
{!isReferencedByAnything && <UnusedContactPointBadge />}
|
||||
<Spacer />
|
||||
<LinkButton
|
||||
tooltipPlacement="top"
|
||||
tooltip={
|
||||
isProvisioned
|
||||
provisioned
|
||||
? t(
|
||||
'alerting.contact-point-header.tooltip-provisioned-contact-points',
|
||||
'Provisioned contact points cannot be edited in the UI'
|
||||
|
||||
+1
-4
@@ -13,7 +13,6 @@ import { setupMswServer } from '../../mockApi';
|
||||
import { grantUserPermissions, mockDataSource } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
import { setupDataSources } from '../../testSetup/datasources';
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
|
||||
import { ContactPoint } from './ContactPoint';
|
||||
@@ -306,9 +305,7 @@ describe('contact points', () => {
|
||||
});
|
||||
|
||||
it('should disable buttons when provisioned', async () => {
|
||||
const { user } = renderWithProvider(
|
||||
<ContactPoint contactPoint={{ ...basicContactPoint, provenance: KnownProvenance.File }} />
|
||||
);
|
||||
const { user } = renderWithProvider(<ContactPoint contactPoint={{ ...basicContactPoint, provisioned: true }} />);
|
||||
|
||||
expect(screen.getByText(/provisioned/i)).toBeInTheDocument();
|
||||
|
||||
|
||||
+10
-10
@@ -50,7 +50,7 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
||||
},
|
||||
},
|
||||
],
|
||||
"provenance": undefined,
|
||||
"provisioned": false,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
@@ -93,7 +93,7 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
||||
},
|
||||
"name": "lotsa-emails",
|
||||
"policies": [],
|
||||
"provenance": undefined,
|
||||
"provisioned": false,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
@@ -129,7 +129,7 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
||||
},
|
||||
"name": "OnCall Conctact point",
|
||||
"policies": [],
|
||||
"provenance": undefined,
|
||||
"provisioned": false,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
@@ -178,7 +178,7 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
||||
},
|
||||
},
|
||||
],
|
||||
"provenance": "api",
|
||||
"provisioned": true,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
@@ -243,7 +243,7 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
||||
},
|
||||
"name": "Slack with multiple channels",
|
||||
"policies": [],
|
||||
"provenance": undefined,
|
||||
"provisioned": false,
|
||||
},
|
||||
],
|
||||
"error": undefined,
|
||||
@@ -301,7 +301,7 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag
|
||||
},
|
||||
},
|
||||
],
|
||||
"provenance": undefined,
|
||||
"provisioned": false,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
@@ -344,7 +344,7 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag
|
||||
},
|
||||
"name": "lotsa-emails",
|
||||
"policies": [],
|
||||
"provenance": undefined,
|
||||
"provisioned": false,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
@@ -383,7 +383,7 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag
|
||||
},
|
||||
"name": "OnCall Conctact point",
|
||||
"policies": [],
|
||||
"provenance": undefined,
|
||||
"provisioned": false,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
@@ -432,7 +432,7 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag
|
||||
},
|
||||
},
|
||||
],
|
||||
"provenance": "api",
|
||||
"provisioned": true,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
@@ -497,7 +497,7 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag
|
||||
},
|
||||
"name": "Slack with multiple channels",
|
||||
"policies": [],
|
||||
"provenance": undefined,
|
||||
"provisioned": false,
|
||||
},
|
||||
],
|
||||
"error": undefined,
|
||||
|
||||
-234
@@ -6,13 +6,10 @@ import { disablePlugin } from 'app/features/alerting/unified/mocks/server/config
|
||||
import { setOnCallIntegrations } from 'app/features/alerting/unified/mocks/server/handlers/plugins/configure-plugins';
|
||||
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import { setupMswServer } from '../../mockApi';
|
||||
import { grantUserPermissions } from '../../mocks';
|
||||
import { setAlertmanagerConfig } from '../../mocks/server/entities/alertmanagers';
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
|
||||
import { useContactPointsWithStatus } from './useContactPoints';
|
||||
|
||||
@@ -72,235 +69,4 @@ describe('useContactPoints', () => {
|
||||
expect(snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Provenance handling', () => {
|
||||
it('should extract provenance when provenance is "api"', async () => {
|
||||
// Set up alertmanager config with a receiver that has API provenance
|
||||
const config: AlertManagerCortexConfig = {
|
||||
template_files: {},
|
||||
alertmanager_config: {
|
||||
receivers: [
|
||||
{
|
||||
name: 'api-provenance-contact-point',
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid-1',
|
||||
name: 'api-provenance-contact-point',
|
||||
type: 'email',
|
||||
disableResolveMessage: false,
|
||||
settings: {
|
||||
addresses: 'test@example.com',
|
||||
},
|
||||
secureFields: {},
|
||||
provenance: 'api', // This will be used by the K8s mock handler
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, config);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useContactPointsWithStatus({
|
||||
alertmanager: GRAFANA_RULES_SOURCE_NAME,
|
||||
fetchPolicies: false,
|
||||
fetchStatuses: false,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
const contactPoint = result.current.contactPoints?.find((cp) => cp.name === 'api-provenance-contact-point');
|
||||
expect(contactPoint).toBeDefined();
|
||||
expect(contactPoint?.provenance).toBe(KnownProvenance.API);
|
||||
});
|
||||
|
||||
it('should extract provenance when provenance is "file"', async () => {
|
||||
const config: AlertManagerCortexConfig = {
|
||||
template_files: {},
|
||||
alertmanager_config: {
|
||||
receivers: [
|
||||
{
|
||||
name: 'file-provenance-contact-point',
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid-2',
|
||||
name: 'file-provenance-contact-point',
|
||||
type: 'email',
|
||||
disableResolveMessage: false,
|
||||
settings: {
|
||||
addresses: 'test@example.com',
|
||||
},
|
||||
secureFields: {},
|
||||
provenance: 'file',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, config);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useContactPointsWithStatus({
|
||||
alertmanager: GRAFANA_RULES_SOURCE_NAME,
|
||||
fetchPolicies: false,
|
||||
fetchStatuses: false,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
const contactPoint = result.current.contactPoints?.find((cp) => cp.name === 'file-provenance-contact-point');
|
||||
expect(contactPoint).toBeDefined();
|
||||
expect(contactPoint?.provenance).toBe(KnownProvenance.File);
|
||||
});
|
||||
|
||||
it('should extract provenance when provenance is "converted_prometheus"', async () => {
|
||||
const config: AlertManagerCortexConfig = {
|
||||
template_files: {},
|
||||
alertmanager_config: {
|
||||
receivers: [
|
||||
{
|
||||
name: 'mimir-provenance-contact-point',
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid-3',
|
||||
name: 'mimir-provenance-contact-point',
|
||||
type: 'email',
|
||||
disableResolveMessage: false,
|
||||
settings: {
|
||||
addresses: 'test@example.com',
|
||||
},
|
||||
secureFields: {},
|
||||
provenance: 'converted_prometheus',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, config);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useContactPointsWithStatus({
|
||||
alertmanager: GRAFANA_RULES_SOURCE_NAME,
|
||||
fetchPolicies: false,
|
||||
fetchStatuses: false,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
const contactPoint = result.current.contactPoints?.find((cp) => cp.name === 'mimir-provenance-contact-point');
|
||||
expect(contactPoint).toBeDefined();
|
||||
expect(contactPoint?.provenance).toBe(KnownProvenance.ConvertedPrometheus);
|
||||
});
|
||||
|
||||
it('should map "none" provenance annotation to undefined', async () => {
|
||||
const config: AlertManagerCortexConfig = {
|
||||
template_files: {},
|
||||
alertmanager_config: {
|
||||
receivers: [
|
||||
{
|
||||
name: 'none-provenance-contact-point',
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid-4',
|
||||
name: 'none-provenance-contact-point',
|
||||
type: 'email',
|
||||
disableResolveMessage: false,
|
||||
settings: {
|
||||
addresses: 'test@example.com',
|
||||
},
|
||||
secureFields: {},
|
||||
// No provenance field - will default to PROVENANCE_NONE in mock handler
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, config);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useContactPointsWithStatus({
|
||||
alertmanager: GRAFANA_RULES_SOURCE_NAME,
|
||||
fetchPolicies: false,
|
||||
fetchStatuses: false,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
const contactPoint = result.current.contactPoints?.find((cp) => cp.name === 'none-provenance-contact-point');
|
||||
expect(contactPoint).toBeDefined();
|
||||
// The mock handler sets PROVENANCE_NONE ('none') when no provenance is found
|
||||
// parseK8sReceiver converts 'none' to undefined
|
||||
expect(contactPoint?.provenance).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle missing annotations gracefully', async () => {
|
||||
// This test verifies that when annotations are undefined, provenance is handled correctly
|
||||
const config: AlertManagerCortexConfig = {
|
||||
template_files: {},
|
||||
alertmanager_config: {
|
||||
receivers: [
|
||||
{
|
||||
name: 'no-annotations-contact-point',
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid-5',
|
||||
name: 'no-annotations-contact-point',
|
||||
type: 'email',
|
||||
disableResolveMessage: false,
|
||||
settings: {
|
||||
addresses: 'test@example.com',
|
||||
},
|
||||
secureFields: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, config);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useContactPointsWithStatus({
|
||||
alertmanager: GRAFANA_RULES_SOURCE_NAME,
|
||||
fetchPolicies: false,
|
||||
fetchStatuses: false,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
const contactPoint = result.current.contactPoints?.find((cp) => cp.name === 'no-annotations-contact-point');
|
||||
expect(contactPoint).toBeDefined();
|
||||
// When annotations are missing, the mock handler should set provenance to undefined
|
||||
expect(contactPoint?.provenance).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver } f
|
||||
import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks';
|
||||
import { cloudNotifierTypes } from 'app/features/alerting/unified/utils/cloud-alertmanager-notifier-types';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils';
|
||||
import { isK8sEntityProvisioned, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils';
|
||||
import { GrafanaManagedContactPoint, Receiver } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { getAPINamespace } from '../../../../../api/utils';
|
||||
@@ -21,9 +21,7 @@ import { useAsync } from '../../hooks/useAsync';
|
||||
import { usePluginBridge } from '../../hooks/usePluginBridge';
|
||||
import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNewAlertmanagerConfig';
|
||||
import { addReceiverAction, deleteReceiverAction, updateReceiverAction } from '../../reducers/alertmanager/receivers';
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
import { getIrmIfPresentOrOnCallPluginId } from '../../utils/config';
|
||||
import { K8sAnnotations } from '../../utils/k8s/constants';
|
||||
|
||||
import { enhanceContactPointsWithMetadata } from './utils';
|
||||
|
||||
@@ -80,13 +78,10 @@ const useOnCallIntegrations = ({ skip }: Skippable = {}) => {
|
||||
type K8sReceiver = ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver;
|
||||
|
||||
const parseK8sReceiver = (item: K8sReceiver): GrafanaManagedContactPoint => {
|
||||
const metadataProvenance = item.metadata.annotations?.[K8sAnnotations.Provenance];
|
||||
const provenance = metadataProvenance === KnownProvenance.None ? undefined : metadataProvenance;
|
||||
|
||||
return {
|
||||
id: item.metadata.name || item.metadata.uid || item.spec.title,
|
||||
name: item.spec.title,
|
||||
provenance: provenance,
|
||||
provisioned: isK8sEntityProvisioned(item),
|
||||
grafana_managed_receiver_configs: item.spec.integrations,
|
||||
metadata: item.metadata,
|
||||
};
|
||||
|
||||
+7
-8
@@ -16,8 +16,7 @@ import {
|
||||
deleteNotificationTemplateAction,
|
||||
updateNotificationTemplateAction,
|
||||
} from '../../reducers/alertmanager/notificationTemplates';
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
import { K8sAnnotations } from '../../utils/k8s/constants';
|
||||
import { K8sAnnotations, PROVENANCE_NONE } from '../../utils/k8s/constants';
|
||||
import { getAnnotation, shouldUseK8sApi } from '../../utils/k8s/utils';
|
||||
import { ensureDefine } from '../../utils/templates';
|
||||
import { TemplateFormValues } from '../receivers/TemplateForm';
|
||||
@@ -80,7 +79,7 @@ function templateGroupsToTemplates(
|
||||
function templateGroupToTemplate(
|
||||
templateGroup: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroup
|
||||
): NotificationTemplate {
|
||||
const provenance = getAnnotation(templateGroup, K8sAnnotations.Provenance) ?? KnownProvenance.None;
|
||||
const provenance = getAnnotation(templateGroup, K8sAnnotations.Provenance) ?? PROVENANCE_NONE;
|
||||
return {
|
||||
// K8s entities should always have a metadata.name property. The type is marked as optional because it's also used in other places
|
||||
uid: templateGroup.metadata.name ?? templateGroup.spec.title,
|
||||
@@ -97,8 +96,8 @@ function amConfigToTemplates(config: AlertManagerCortexConfig): NotificationTemp
|
||||
uid: title,
|
||||
title,
|
||||
content,
|
||||
// Undefined, null or empty string should be converted to KnownProvenance.None
|
||||
provenance: (config.template_file_provenances ?? {})[title] || KnownProvenance.None,
|
||||
// Undefined, null or empty string should be converted to PROVENANCE_NONE
|
||||
provenance: (config.template_file_provenances ?? {})[title] || PROVENANCE_NONE,
|
||||
missing: !templates.includes(title),
|
||||
}));
|
||||
}
|
||||
@@ -273,7 +272,7 @@ export function useValidateNotificationTemplate({
|
||||
}
|
||||
|
||||
interface NotificationTemplateMetadata {
|
||||
provenance?: string;
|
||||
isProvisioned: boolean;
|
||||
}
|
||||
|
||||
export function useNotificationTemplateMetadata(
|
||||
@@ -281,11 +280,11 @@ export function useNotificationTemplateMetadata(
|
||||
): NotificationTemplateMetadata {
|
||||
if (!template) {
|
||||
return {
|
||||
provenance: KnownProvenance.None,
|
||||
isProvisioned: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
provenance: template.provenance,
|
||||
isProvisioned: Boolean(template.provenance) && template.provenance !== PROVENANCE_NONE,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { GrafanaManagedContactPoint } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
import { ReceiverTypes } from '../receivers/grafanaAppReceivers/onCall/onCall';
|
||||
|
||||
import { RECEIVER_META_KEY, RECEIVER_PLUGIN_META_KEY } from './constants';
|
||||
import {
|
||||
ReceiverConfigWithMetadata,
|
||||
enhanceContactPointsWithMetadata,
|
||||
getReceiverDescription,
|
||||
isAutoGeneratedPolicy,
|
||||
summarizeEmailAddresses,
|
||||
@@ -132,110 +128,3 @@ describe('summarizeEmailAddresses', () => {
|
||||
expect(summarizeEmailAddresses('foo@foo.com\n bar@bar.com ')).toBe(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enhanceContactPointsWithMetadata', () => {
|
||||
it('should extract provenance from receiver configs when contact point has no provenance', () => {
|
||||
const contactPoint: GrafanaManagedContactPoint = {
|
||||
name: 'test-contact-point',
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid',
|
||||
name: 'test-contact-point',
|
||||
type: 'email',
|
||||
settings: { addresses: 'test@example.com' },
|
||||
secureFields: {},
|
||||
provenance: KnownProvenance.API,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const enhanced = enhanceContactPointsWithMetadata({
|
||||
contactPoints: [contactPoint],
|
||||
notifiers: [],
|
||||
status: [],
|
||||
});
|
||||
|
||||
expect(enhanced[0].provenance).toBe(KnownProvenance.API);
|
||||
});
|
||||
|
||||
it('should prefer contact point provenance over receiver config provenance', () => {
|
||||
const contactPoint: GrafanaManagedContactPoint = {
|
||||
name: 'test-contact-point',
|
||||
provenance: KnownProvenance.File, // Provenance on contact point (from K8s)
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid',
|
||||
name: 'test-contact-point',
|
||||
type: 'email',
|
||||
settings: { addresses: 'test@example.com' },
|
||||
secureFields: {},
|
||||
provenance: KnownProvenance.API, // Different provenance on receiver config
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const enhanced = enhanceContactPointsWithMetadata({
|
||||
contactPoints: [contactPoint],
|
||||
notifiers: [],
|
||||
status: [],
|
||||
});
|
||||
|
||||
expect(enhanced[0].provenance).toBe(KnownProvenance.File);
|
||||
});
|
||||
|
||||
it('should extract provenance from first receiver config that has it', () => {
|
||||
const contactPoint: GrafanaManagedContactPoint = {
|
||||
name: 'test-contact-point',
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid-1',
|
||||
name: 'test-contact-point',
|
||||
type: 'email',
|
||||
settings: { addresses: 'test@example.com' },
|
||||
secureFields: {},
|
||||
// No provenance on first receiver
|
||||
},
|
||||
{
|
||||
uid: 'test-uid-2',
|
||||
name: 'test-contact-point',
|
||||
type: 'slack',
|
||||
settings: { recipient: '#channel' },
|
||||
secureFields: {},
|
||||
provenance: KnownProvenance.ConvertedPrometheus, // Provenance on second receiver
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const enhanced = enhanceContactPointsWithMetadata({
|
||||
contactPoints: [contactPoint],
|
||||
notifiers: [],
|
||||
status: [],
|
||||
});
|
||||
|
||||
expect(enhanced[0].provenance).toBe(KnownProvenance.ConvertedPrometheus);
|
||||
});
|
||||
|
||||
it('should have undefined provenance when neither contact point nor receiver configs have provenance', () => {
|
||||
const contactPoint: GrafanaManagedContactPoint = {
|
||||
name: 'test-contact-point',
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid',
|
||||
name: 'test-contact-point',
|
||||
type: 'email',
|
||||
settings: { addresses: 'test@example.com' },
|
||||
secureFields: {},
|
||||
// No provenance
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const enhanced = enhanceContactPointsWithMetadata({
|
||||
contactPoints: [contactPoint],
|
||||
notifiers: [],
|
||||
status: [],
|
||||
});
|
||||
|
||||
expect(enhanced[0].provenance).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,16 +146,9 @@ export function enhanceContactPointsWithMetadata({
|
||||
|
||||
const id = getContactPointIdentifier(contactPoint);
|
||||
|
||||
// Extract provenance from contactPoint first; else, search in its receivers
|
||||
const contactPointProvenance =
|
||||
'provenance' in contactPoint && contactPoint.provenance !== undefined
|
||||
? contactPoint.provenance
|
||||
: receivers.find((receiver) => Boolean(receiver.provenance))?.provenance;
|
||||
|
||||
return {
|
||||
...contactPoint,
|
||||
id,
|
||||
provenance: contactPointProvenance,
|
||||
policies:
|
||||
alertmanagerConfiguration && usedContactPointsByName && (usedContactPointsByName[contactPoint.name] ?? []),
|
||||
grafana_managed_receiver_configs: receivers.map((receiver, index) => {
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { ReactNode } from 'react';
|
||||
import { getWrapper } from 'test/test-utils';
|
||||
|
||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import { grantUserPermissions } from 'app/features/alerting/unified/mocks';
|
||||
import {
|
||||
TIME_INTERVAL_NAME_FILE_PROVISIONED,
|
||||
TIME_INTERVAL_NAME_HAPPY_PATH,
|
||||
} from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import { useGetMuteTiming, useMuteTimings } from './useMuteTimings';
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => {
|
||||
const ProviderWrapper = getWrapper({ renderWithRouter: true });
|
||||
return <ProviderWrapper>{children}</ProviderWrapper>;
|
||||
};
|
||||
|
||||
setupMswServer();
|
||||
|
||||
describe('useMuteTimings', () => {
|
||||
beforeEach(() => {
|
||||
grantUserPermissions([AccessControlAction.AlertingNotificationsRead]);
|
||||
});
|
||||
|
||||
describe('useMuteTimings', () => {
|
||||
it('should return mute timings with correct data structure', async () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useMuteTimings({
|
||||
alertmanager: GRAFANA_RULES_SOURCE_NAME,
|
||||
skip: false,
|
||||
}),
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.data).toBeDefined();
|
||||
expect(Array.isArray(result.current.data)).toBe(true);
|
||||
|
||||
const timings = result.current.data!;
|
||||
expect(timings.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify structure of first timing
|
||||
const firstTiming = timings[0];
|
||||
expect(firstTiming).toHaveProperty('id');
|
||||
expect(firstTiming).toHaveProperty('name');
|
||||
expect(firstTiming).toHaveProperty('time_intervals');
|
||||
expect(typeof firstTiming.id).toBe('string');
|
||||
expect(typeof firstTiming.name).toBe('string');
|
||||
expect(Array.isArray(firstTiming.time_intervals)).toBe(true);
|
||||
});
|
||||
|
||||
it('should correctly identify provisioned intervals', async () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useMuteTimings({
|
||||
alertmanager: GRAFANA_RULES_SOURCE_NAME,
|
||||
skip: false,
|
||||
}),
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
const timings = result.current.data!;
|
||||
|
||||
// Find the provisioned interval
|
||||
const provisionedTiming = timings.find((t) => t.name === TIME_INTERVAL_NAME_FILE_PROVISIONED);
|
||||
expect(provisionedTiming).toBeDefined();
|
||||
expect(provisionedTiming?.provisioned).toBe(true);
|
||||
|
||||
// Find the non-provisioned interval
|
||||
const nonProvisionedTiming = timings.find((t) => t.name === TIME_INTERVAL_NAME_HAPPY_PATH);
|
||||
expect(nonProvisionedTiming).toBeDefined();
|
||||
expect(nonProvisionedTiming?.provisioned).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useGetMuteTiming', () => {
|
||||
it('should return single mute timing by name for editing', async () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useGetMuteTiming({
|
||||
alertmanager: GRAFANA_RULES_SOURCE_NAME,
|
||||
name: TIME_INTERVAL_NAME_HAPPY_PATH,
|
||||
}),
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.data).toBeDefined();
|
||||
expect(result.current.data?.name).toBe(TIME_INTERVAL_NAME_HAPPY_PATH);
|
||||
expect(result.current.data?.id).toBe(TIME_INTERVAL_NAME_HAPPY_PATH);
|
||||
expect(result.current.data).toHaveProperty('time_intervals');
|
||||
expect(result.current.isError).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
IoK8SApimachineryPkgApisMetaV1ObjectMeta,
|
||||
} from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen';
|
||||
import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks';
|
||||
import { PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
import {
|
||||
isK8sEntityProvisioned,
|
||||
isProvisionedResource,
|
||||
shouldUseK8sApi,
|
||||
stringifyFieldSelector,
|
||||
} from 'app/features/alerting/unified/utils/k8s/utils';
|
||||
@@ -62,7 +62,7 @@ const parseAmTimeInterval: (interval: MuteTimeInterval, provenance: string) => M
|
||||
return {
|
||||
...interval,
|
||||
id: interval.name,
|
||||
provisioned: isProvisionedResource(provenance),
|
||||
provisioned: Boolean(provenance && provenance !== PROVENANCE_NONE),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+2
-1
@@ -11,7 +11,7 @@ import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alertin
|
||||
import { FormAmRoute } from 'app/features/alerting/unified/types/amroutes';
|
||||
import { addUniqueIdentifierToRoute } from 'app/features/alerting/unified/utils/amroutes';
|
||||
import { getErrorCode, stringifyErrorLike } from 'app/features/alerting/unified/utils/misc';
|
||||
import { ObjectMatcher, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { ObjectMatcher, ROUTES_META_SYMBOL, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { anyOfRequestState, isError } from '../../hooks/useAsync';
|
||||
import { useAlertmanager } from '../../state/AlertmanagerContext';
|
||||
@@ -244,6 +244,7 @@ export const NotificationPoliciesList = () => {
|
||||
currentRoute={defaults(rootRoute, TIMING_OPTIONS_DEFAULTS)}
|
||||
contactPointsState={contactPointsState.receivers}
|
||||
readOnly={!hasConfigurationAPI}
|
||||
provisioned={rootRoute[ROUTES_META_SYMBOL]?.provisioned}
|
||||
alertManagerSourceName={selectedAlertmanager}
|
||||
onAddPolicy={openAddModal}
|
||||
onEditPolicy={openEditModal}
|
||||
|
||||
+12
-97
@@ -11,14 +11,12 @@ import {
|
||||
AlertmanagerGroup,
|
||||
MatcherOperator,
|
||||
ObjectMatcher,
|
||||
ROUTES_META_SYMBOL,
|
||||
RouteWithID,
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { useAlertmanagerAbilities } from '../../hooks/useAbilities';
|
||||
import { mockReceiversState } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
|
||||
import {
|
||||
@@ -333,84 +331,6 @@ describe('Policy', () => {
|
||||
const customPolicy = screen.getByTestId('am-route-container');
|
||||
expect(within(customPolicy).getByTestId('matches-all')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows correct badge when policy has file provenance', () => {
|
||||
const mockRoute: RouteWithID = {
|
||||
id: 'test-route',
|
||||
receiver: 'test-receiver',
|
||||
routes: [],
|
||||
[ROUTES_META_SYMBOL]: { provenance: KnownProvenance.File },
|
||||
};
|
||||
|
||||
renderPolicy(
|
||||
<Policy
|
||||
readOnly
|
||||
isDefaultPolicy
|
||||
currentRoute={mockRoute}
|
||||
contactPointsState={mockReceiversState()}
|
||||
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
||||
onEditPolicy={noop}
|
||||
onAddPolicy={noop}
|
||||
onDeletePolicy={noop}
|
||||
onShowAlertInstances={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
const badge = screen.getByText('Provisioned');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows correct badge when policy has converted_prometheus provenance', () => {
|
||||
const mockRoute: RouteWithID = {
|
||||
id: 'test-route',
|
||||
receiver: 'test-receiver',
|
||||
routes: [],
|
||||
[ROUTES_META_SYMBOL]: { provenance: KnownProvenance.ConvertedPrometheus },
|
||||
};
|
||||
|
||||
renderPolicy(
|
||||
<Policy
|
||||
readOnly
|
||||
isDefaultPolicy
|
||||
currentRoute={mockRoute}
|
||||
contactPointsState={mockReceiversState()}
|
||||
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
||||
onEditPolicy={noop}
|
||||
onAddPolicy={noop}
|
||||
onDeletePolicy={noop}
|
||||
onShowAlertInstances={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
const badge = screen.getByText('Imported');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('correctly identifies provisioned status from ROUTES_META_SYMBOL', () => {
|
||||
const mockRoute: RouteWithID = {
|
||||
id: 'test-route',
|
||||
receiver: 'test-receiver',
|
||||
routes: [],
|
||||
[ROUTES_META_SYMBOL]: { provenance: KnownProvenance.File },
|
||||
};
|
||||
|
||||
renderPolicy(
|
||||
<Policy
|
||||
isDefaultPolicy
|
||||
currentRoute={mockRoute}
|
||||
contactPointsState={mockReceiversState()}
|
||||
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
||||
onEditPolicy={noop}
|
||||
onAddPolicy={noop}
|
||||
onDeletePolicy={noop}
|
||||
onShowAlertInstances={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Provisioned')).toBeInTheDocument();
|
||||
// Verify add/edit buttons are disabled
|
||||
expect(screen.getByRole('button', { name: /new child policy/i })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
// Doesn't matter which path the routes use, it just needs to match the initialEntries history entry to render the element
|
||||
@@ -502,52 +422,47 @@ describe('useCreateDropdownMenuActions', () => {
|
||||
{
|
||||
isAutoGenerated: false,
|
||||
isDefaultPolicy: true,
|
||||
provenance: undefined,
|
||||
provisioned: false,
|
||||
expectedMenu: ['edit-policy', 'export-policy'],
|
||||
},
|
||||
{
|
||||
isAutoGenerated: false,
|
||||
isDefaultPolicy: true,
|
||||
provenance: KnownProvenance.File,
|
||||
provisioned: true,
|
||||
expectedMenu: ['edit-policy', 'export-policy'],
|
||||
},
|
||||
{
|
||||
isAutoGenerated: false,
|
||||
isDefaultPolicy: false,
|
||||
provenance: undefined,
|
||||
provisioned: false,
|
||||
expectedMenu: ['edit-policy', 'delete-policy'],
|
||||
},
|
||||
{
|
||||
isAutoGenerated: false,
|
||||
isDefaultPolicy: false,
|
||||
provenance: KnownProvenance.File,
|
||||
provisioned: true,
|
||||
expectedMenu: ['edit-policy', 'delete-policy'],
|
||||
},
|
||||
{ isAutoGenerated: true, isDefaultPolicy: true, provenance: KnownProvenance.File, expectedMenu: ['edit-policy'] },
|
||||
{ isAutoGenerated: true, isDefaultPolicy: false, provenance: undefined, expectedMenu: ['edit-policy'] },
|
||||
{ isAutoGenerated: true, isDefaultPolicy: true, provenance: undefined, expectedMenu: ['edit-policy'] },
|
||||
{ isAutoGenerated: true, isDefaultPolicy: false, provenance: KnownProvenance.File, expectedMenu: ['edit-policy'] },
|
||||
{ isAutoGenerated: true, isDefaultPolicy: true, provisioned: true, expectedMenu: ['edit-policy'] },
|
||||
{ isAutoGenerated: true, isDefaultPolicy: false, provisioned: false, expectedMenu: ['edit-policy'] },
|
||||
{ isAutoGenerated: true, isDefaultPolicy: true, provisioned: false, expectedMenu: ['edit-policy'] },
|
||||
{ isAutoGenerated: true, isDefaultPolicy: false, provisioned: true, expectedMenu: ['edit-policy'] },
|
||||
];
|
||||
|
||||
testCases.forEach(({ isAutoGenerated, isDefaultPolicy, provenance, expectedMenu }) => {
|
||||
const provisionedStatus = provenance ? 'provisioned' : 'not provisioned';
|
||||
it(`Having all the permissions returns ${expectedMenu.length} menu items for isAutoGenerated=${isAutoGenerated}, isDefaultPolicy=${isDefaultPolicy}, ${provisionedStatus}`, () => {
|
||||
testCases.forEach(({ isAutoGenerated, isDefaultPolicy, provisioned, expectedMenu }) => {
|
||||
it(`Having all the permissions returns ${expectedMenu.length} menu items for isAutoGenerated=${isAutoGenerated}, isDefaultPolicy=${isDefaultPolicy}, provisioned=${provisioned}`, () => {
|
||||
useAlertmanagerAbilitiesMock.mockReturnValue([
|
||||
[true, true],
|
||||
[true, true],
|
||||
[true, true],
|
||||
]);
|
||||
// Create route with provenance in metadata or top-level to match real usage
|
||||
const routeWithProvenance: RouteWithID = provenance
|
||||
? { ...currentRoute, [ROUTES_META_SYMBOL]: { provenance } }
|
||||
: currentRoute;
|
||||
const { result } = renderHook(() =>
|
||||
useCreateDropdownMenuActions(
|
||||
isAutoGenerated,
|
||||
isDefaultPolicy,
|
||||
provenance,
|
||||
provisioned,
|
||||
openDetailModal,
|
||||
routeWithProvenance,
|
||||
currentRoute,
|
||||
toggleShowExportDrawer,
|
||||
onDeletePolicy
|
||||
)
|
||||
|
||||
@@ -31,14 +31,12 @@ import {
|
||||
AlertmanagerGroup,
|
||||
MatcherOperator,
|
||||
ObjectMatcher,
|
||||
ROUTES_META_SYMBOL,
|
||||
Receiver,
|
||||
RouteWithID,
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { AlertmanagerAction, useAlertmanagerAbilities, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
||||
import { getAmMatcherFormatter } from '../../utils/alertmanager';
|
||||
import { isProvisionedResource } from '../../utils/k8s/utils';
|
||||
import { MatcherFormatter, normalizeMatchers } from '../../utils/matchers';
|
||||
import { createContactPointLink, createContactPointSearchLink, createMuteTimingLink } from '../../utils/misc';
|
||||
import { routeAdapter } from '../../utils/routeAdapter';
|
||||
@@ -62,6 +60,7 @@ interface PolicyComponentProps {
|
||||
receivers?: Receiver[];
|
||||
contactPointsState?: ReceiversState;
|
||||
readOnly?: boolean;
|
||||
provisioned?: boolean;
|
||||
inheritedProperties?: InheritableProperties;
|
||||
routesMatchingFilters?: RoutesMatchingFilters;
|
||||
|
||||
@@ -89,6 +88,7 @@ const Policy = (props: PolicyComponentProps) => {
|
||||
receivers = [],
|
||||
contactPointsState,
|
||||
readOnly = false,
|
||||
provisioned = false,
|
||||
alertManagerSourceName,
|
||||
currentRoute,
|
||||
inheritedProperties,
|
||||
@@ -107,10 +107,6 @@ const Policy = (props: PolicyComponentProps) => {
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
// Derive provenance from route metadata or top-level (consistent with child handling)
|
||||
const provenance = currentRoute[ROUTES_META_SYMBOL]?.provenance ?? currentRoute.provenance;
|
||||
const provisioned = isProvisionedResource(provenance);
|
||||
|
||||
const contactPoint = currentRoute.receiver;
|
||||
const continueMatching = currentRoute.continue ?? false;
|
||||
|
||||
@@ -185,7 +181,7 @@ const Policy = (props: PolicyComponentProps) => {
|
||||
const dropdownMenuActions: JSX.Element[] = useCreateDropdownMenuActions(
|
||||
isAutoGenerated,
|
||||
isDefaultPolicy,
|
||||
provenance,
|
||||
provisioned,
|
||||
onEditPolicy,
|
||||
currentRoute,
|
||||
toggleShowExportDrawer,
|
||||
@@ -259,7 +255,7 @@ const Policy = (props: PolicyComponentProps) => {
|
||||
<Spacer />
|
||||
{/* TODO maybe we should move errors to the gutter instead? */}
|
||||
{errors.length > 0 && <Errors errors={errors} />}
|
||||
{provisioned && <ProvisioningBadge tooltip provenance={provenance} />}
|
||||
{provisioned && <ProvisioningBadge />}
|
||||
<Stack direction="row" gap={0.5}>
|
||||
{!isAutoGenerated && !readOnly && (
|
||||
<Authorize actions={[AlertmanagerAction.CreateNotificationPolicy]}>
|
||||
@@ -379,6 +375,7 @@ const Policy = (props: PolicyComponentProps) => {
|
||||
routesMatchingFilters={routesMatchingFilters}
|
||||
matchingInstancesPreview={matchingInstancesPreview}
|
||||
isAutoGenerated={isThisChildAutoGenerated}
|
||||
provisioned={provisioned}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -543,7 +540,7 @@ function MetadataRow({
|
||||
export const useCreateDropdownMenuActions = (
|
||||
isAutoGenerated: boolean,
|
||||
isDefaultPolicy: boolean,
|
||||
provenance: string | undefined,
|
||||
provisioned: boolean,
|
||||
onEditPolicy: (route: RouteWithID, isDefault?: boolean, readOnly?: boolean) => void,
|
||||
currentRoute: RouteWithID,
|
||||
toggleShowExportDrawer: () => void,
|
||||
@@ -559,9 +556,6 @@ export const useCreateDropdownMenuActions = (
|
||||
AlertmanagerAction.ExportNotificationPolicies,
|
||||
]);
|
||||
|
||||
// Compute provisioned status from provenance
|
||||
const provisioned = isProvisionedResource(provenance);
|
||||
|
||||
const dropdownMenuActions = [];
|
||||
const showExportAction = exportPoliciesAllowed && exportPoliciesSupported && isDefaultPolicy && !isAutoGenerated;
|
||||
const showEditAction = updatePoliciesSupported && updatePoliciesAllowed;
|
||||
|
||||
+1
-90
@@ -1,15 +1,9 @@
|
||||
import { MatcherOperator, ROUTES_META_SYMBOL, Route } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route } from '../../openapi/routesApi.gen';
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
import { ROOT_ROUTE_NAME } from '../../utils/k8s/constants';
|
||||
|
||||
import {
|
||||
createKubernetesRoutingTreeSpec,
|
||||
isRouteProvisioned,
|
||||
k8sSubRouteToRoute,
|
||||
routeToK8sSubRoute,
|
||||
} from './useNotificationPolicyRoute';
|
||||
import { createKubernetesRoutingTreeSpec, k8sSubRouteToRoute, routeToK8sSubRoute } from './useNotificationPolicyRoute';
|
||||
|
||||
test('k8sSubRouteToRoute', () => {
|
||||
const input: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route = {
|
||||
@@ -121,86 +115,3 @@ test('createKubernetesRoutingTreeSpec', () => {
|
||||
expect(tree.metadata.name).toBe(ROOT_ROUTE_NAME);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('isRouteProvisioned', () => {
|
||||
it('returns false when route has no provenance', () => {
|
||||
const route: Route = {
|
||||
receiver: 'test-receiver',
|
||||
};
|
||||
|
||||
expect(isRouteProvisioned(route)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns false when route has KnownProvenance.None in metadata', () => {
|
||||
const route: Route = {
|
||||
receiver: 'test-receiver',
|
||||
[ROUTES_META_SYMBOL]: {
|
||||
provenance: KnownProvenance.None,
|
||||
},
|
||||
};
|
||||
|
||||
expect(isRouteProvisioned(route)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns false when route has KnownProvenance.None at top level', () => {
|
||||
const route: Route = {
|
||||
receiver: 'test-receiver',
|
||||
provenance: KnownProvenance.None,
|
||||
};
|
||||
expect(isRouteProvisioned(route)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns true when route has file provenance in metadata', () => {
|
||||
const route: Route = {
|
||||
receiver: 'test-receiver',
|
||||
[ROUTES_META_SYMBOL]: {
|
||||
provenance: KnownProvenance.File,
|
||||
},
|
||||
};
|
||||
|
||||
expect(isRouteProvisioned(route)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true when route has api provenance in metadata', () => {
|
||||
const route: Route = {
|
||||
receiver: 'test-receiver',
|
||||
[ROUTES_META_SYMBOL]: {
|
||||
provenance: KnownProvenance.API,
|
||||
},
|
||||
};
|
||||
|
||||
expect(isRouteProvisioned(route)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true when route has converted_prometheus provenance in metadata', () => {
|
||||
const route: Route = {
|
||||
receiver: 'test-receiver',
|
||||
[ROUTES_META_SYMBOL]: {
|
||||
provenance: KnownProvenance.ConvertedPrometheus,
|
||||
},
|
||||
};
|
||||
|
||||
expect(isRouteProvisioned(route)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true when route has file provenance at top level', () => {
|
||||
const route: Route = {
|
||||
receiver: 'test-receiver',
|
||||
provenance: KnownProvenance.File,
|
||||
};
|
||||
|
||||
expect(isRouteProvisioned(route)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('falls back to top-level provenance when metadata provenance is missing', () => {
|
||||
const route: Route = {
|
||||
receiver: 'test-receiver',
|
||||
provenance: KnownProvenance.File,
|
||||
[ROUTES_META_SYMBOL]: {
|
||||
provenance: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
expect(isRouteProvisioned(route)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
+4
-10
@@ -22,8 +22,8 @@ import {
|
||||
} from '../../reducers/alertmanager/notificationPolicyRoutes';
|
||||
import { FormAmRoute } from '../../types/amroutes';
|
||||
import { addUniqueIdentifierToRoute } from '../../utils/amroutes';
|
||||
import { K8sAnnotations, ROOT_ROUTE_NAME } from '../../utils/k8s/constants';
|
||||
import { getAnnotation, isProvisionedResource, shouldUseK8sApi } from '../../utils/k8s/utils';
|
||||
import { PROVENANCE_NONE, ROOT_ROUTE_NAME } from '../../utils/k8s/constants';
|
||||
import { isK8sEntityProvisioned, shouldUseK8sApi } from '../../utils/k8s/utils';
|
||||
import { routeAdapter } from '../../utils/routeAdapter';
|
||||
import {
|
||||
InsertPosition,
|
||||
@@ -33,11 +33,6 @@ import {
|
||||
omitRouteFromRouteTree,
|
||||
} from '../../utils/routeTree';
|
||||
|
||||
export function isRouteProvisioned(route: Route): boolean {
|
||||
const provenance = route[ROUTES_META_SYMBOL]?.provenance ?? route.provenance;
|
||||
return isProvisionedResource(provenance);
|
||||
}
|
||||
|
||||
const k8sRoutesToRoutesMemoized = memoize(k8sRoutesToRoutes, { maxSize: 1 });
|
||||
|
||||
const {
|
||||
@@ -87,7 +82,7 @@ const parseAmConfigRoute = memoize((route: Route): Route => {
|
||||
return {
|
||||
...route,
|
||||
[ROUTES_META_SYMBOL]: {
|
||||
provenance: route.provenance,
|
||||
provisioned: Boolean(route.provenance && route.provenance !== PROVENANCE_NONE),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -237,11 +232,10 @@ function k8sRoutesToRoutes(routes: ComGithubGrafanaGrafanaPkgApisAlertingNotific
|
||||
...route.spec.defaults,
|
||||
routes: route.spec.routes?.map(k8sSubRouteToRoute),
|
||||
[ROUTES_META_SYMBOL]: {
|
||||
provenance: getAnnotation(route, K8sAnnotations.Provenance),
|
||||
provisioned: isK8sEntityProvisioned(route),
|
||||
resourceVersion: route.metadata.resourceVersion,
|
||||
name: route.metadata.name,
|
||||
},
|
||||
provenance: getAnnotation(route, K8sAnnotations.Provenance),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import { AITemplateButtonComponent } from '../../enterprise-components/AI/AIGenTemplateButton/addAITemplateButton';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { isProvisionedResource } from '../../utils/k8s/utils';
|
||||
import { makeAMLink, stringifyErrorLike } from '../../utils/misc';
|
||||
import { EditorColumnHeader } from '../EditorColumnHeader';
|
||||
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
|
||||
@@ -123,8 +122,7 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
|
||||
// AI feedback state
|
||||
const [aiGeneratedTemplate, setAiGeneratedTemplate] = useState(false);
|
||||
|
||||
const { provenance } = useNotificationTemplateMetadata(originalTemplate);
|
||||
const isProvisioned = isProvisionedResource(provenance);
|
||||
const { isProvisioned } = useNotificationTemplateMetadata(originalTemplate);
|
||||
const originalTemplatePrefill: TemplateFormValues | undefined = originalTemplate
|
||||
? { title: originalTemplate.title, content: originalTemplate.content }
|
||||
: undefined;
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { render, screen, within } from 'test/test-utils';
|
||||
|
||||
import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import { setupMswServer } from '../../mockApi';
|
||||
import { grantUserPermissions } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { NotificationTemplate } from '../contact-points/useNotificationTemplates';
|
||||
|
||||
import { TemplatesTable } from './TemplatesTable';
|
||||
|
||||
const mockTemplates: Array<Partial<NotificationTemplate>> = [
|
||||
{
|
||||
uid: 'mimir-template',
|
||||
title: 'mimir-template',
|
||||
content: '{{ define "mimir-template" }}Template from Mimir{{ end }}',
|
||||
provenance: KnownProvenance.ConvertedPrometheus,
|
||||
},
|
||||
{
|
||||
uid: 'file-template',
|
||||
title: 'file-template',
|
||||
content: '{{ define "file-template" }}File provisioned template{{ end }}',
|
||||
provenance: KnownProvenance.File,
|
||||
},
|
||||
{
|
||||
uid: 'api-template',
|
||||
title: 'api-template',
|
||||
content: '{{ define "api-template" }}API provisioned template{{ end }}',
|
||||
provenance: KnownProvenance.API,
|
||||
},
|
||||
{
|
||||
uid: 'no-provenance-template',
|
||||
title: 'no-provenance-template',
|
||||
content: '{{ define "no-provenance-template" }}No provenance template{{ end }}',
|
||||
provenance: KnownProvenance.None,
|
||||
},
|
||||
{
|
||||
uid: 'undefined-provenance-template',
|
||||
title: 'undefined-provenance-template',
|
||||
content: '{{ define "undefined-provenance-template" }}Undefined provenance template{{ end }}',
|
||||
provenance: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const renderWithProvider = (templates: Array<Partial<NotificationTemplate>>) => {
|
||||
return render(
|
||||
<AlertmanagerProvider accessType={'notification'}>
|
||||
<TemplatesTable alertManagerName={GRAFANA_RULES_SOURCE_NAME} templates={templates as NotificationTemplate[]} />
|
||||
<AppNotificationList />
|
||||
</AlertmanagerProvider>
|
||||
);
|
||||
};
|
||||
|
||||
setupMswServer();
|
||||
|
||||
describe('TemplatesTable', () => {
|
||||
beforeEach(() => {
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsWrite,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
AccessControlAction.AlertingNotificationsExternalWrite,
|
||||
]);
|
||||
});
|
||||
|
||||
it('shows "Imported" badge for templates with converted_prometheus provenance', () => {
|
||||
const templates = [mockTemplates[0]]; // mimir-template
|
||||
renderWithProvider(templates);
|
||||
|
||||
const templateRow = screen.getByRole('row', { name: /mimir-template/i });
|
||||
const badge = within(templateRow).getByText('Imported');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Provisioned" badge for templates with other provenance', () => {
|
||||
// api and file templates
|
||||
[mockTemplates[1], mockTemplates[2]].forEach((template) => {
|
||||
renderWithProvider([template]);
|
||||
|
||||
const templateRow = screen.getByRole('row', { name: new RegExp(template.title ?? '', 'i') });
|
||||
const badge = within(templateRow).getByText('Provisioned');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show badge for templates with KnownProvenance.None or empty string provenance', () => {
|
||||
// no-provenance-template and undefined-provenance-template
|
||||
[mockTemplates[3], mockTemplates[4]].forEach((template) => {
|
||||
renderWithProvider([template]);
|
||||
|
||||
const templateRow = screen.getByRole('row', { name: new RegExp(template.title ?? '', 'i') });
|
||||
expect(within(templateRow).queryByText('Provisioned')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,6 @@ import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/d
|
||||
import { Authorize } from '../../components/Authorize';
|
||||
import { AlertmanagerAction } from '../../hooks/useAbilities';
|
||||
import { getAlertTableStyles } from '../../styles/table';
|
||||
import { isProvisionedResource } from '../../utils/k8s/utils';
|
||||
import { makeAMLink, stringifyErrorLike } from '../../utils/misc';
|
||||
import { CollapseToggle } from '../CollapseToggle';
|
||||
import { DetailsField } from '../DetailsField';
|
||||
@@ -129,8 +128,7 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic
|
||||
const isGrafanaAlertmanager = alertManagerName === GRAFANA_RULES_SOURCE_NAME;
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const { provenance } = useNotificationTemplateMetadata(notificationTemplate);
|
||||
const isProvisioned = isProvisionedResource(provenance);
|
||||
const { isProvisioned } = useNotificationTemplateMetadata(notificationTemplate);
|
||||
|
||||
const { uid, title: name, content: template, missing } = notificationTemplate;
|
||||
const misconfiguredBadgeText = t('alerting.templates.misconfigured-badge-text', 'Misconfigured');
|
||||
@@ -141,7 +139,7 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic
|
||||
<CollapseToggle isCollapsed={!isExpanded} onToggle={() => setIsExpanded(!isExpanded)} />
|
||||
</td>
|
||||
<td>
|
||||
{name} {isProvisioned && <ProvisioningBadge tooltip provenance={provenance} />}{' '}
|
||||
{name} {isProvisioned && <ProvisioningBadge />}{' '}
|
||||
{missing && !isGrafanaAlertmanager && (
|
||||
<Tooltip
|
||||
content={
|
||||
|
||||
+6
-9
@@ -9,11 +9,7 @@ import {
|
||||
} from 'app/features/alerting/unified/components/contact-points/useContactPoints';
|
||||
import { showManageContactPointPermissions } from 'app/features/alerting/unified/components/contact-points/utils';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import {
|
||||
canEditEntity,
|
||||
canModifyProtectedEntity,
|
||||
isProvisionedResource,
|
||||
} from 'app/features/alerting/unified/utils/k8s/utils';
|
||||
import { canEditEntity, canModifyProtectedEntity } from 'app/features/alerting/unified/utils/k8s/utils';
|
||||
import {
|
||||
GrafanaManagedContactPoint,
|
||||
GrafanaManagedReceiverConfig,
|
||||
@@ -131,8 +127,7 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
|
||||
// If there is no contact point it means we're creating a new one, so scoped permissions doesn't exist yet
|
||||
const hasScopedEditPermissions = contactPoint ? canEditEntity(contactPoint) : true;
|
||||
const hasScopedEditProtectedPermissions = contactPoint ? canModifyProtectedEntity(contactPoint) : true;
|
||||
const isProvisioned = isProvisionedResource(contactPoint?.provenance);
|
||||
const isEditable = !readOnly && hasScopedEditPermissions && !isProvisioned;
|
||||
const isEditable = !readOnly && hasScopedEditPermissions && !contactPoint?.provisioned;
|
||||
const isTestable = !readOnly;
|
||||
const canEditProtectedFields = editMode ? hasScopedEditProtectedPermissions : true;
|
||||
|
||||
@@ -175,8 +170,10 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isProvisioned && hasLegacyIntegrations(contactPoint, grafanaNotifiers) && <ImportedContactPointAlert />}
|
||||
{isProvisioned && !hasLegacyIntegrations(contactPoint, grafanaNotifiers) && (
|
||||
{contactPoint?.provisioned && hasLegacyIntegrations(contactPoint, grafanaNotifiers) && (
|
||||
<ImportedContactPointAlert />
|
||||
)}
|
||||
{contactPoint?.provisioned && !hasLegacyIntegrations(contactPoint, grafanaNotifiers) && (
|
||||
<ProvisioningAlert resource={ProvisionedResource.ContactPoint} />
|
||||
)}
|
||||
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ import { grantUserPermissions } from 'app/features/alerting/unified/mocks';
|
||||
import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
|
||||
import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext';
|
||||
import { NotificationChannelOption } from 'app/features/alerting/unified/types/alerting';
|
||||
import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
import { DEFAULT_TEMPLATES } from 'app/features/alerting/unified/utils/template-constants';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('getTemplateOptions function', () => {
|
||||
uid: title,
|
||||
title,
|
||||
content,
|
||||
provenance: KnownProvenance.None,
|
||||
provenance: PROVENANCE_NONE,
|
||||
};
|
||||
});
|
||||
const defaultTemplates = parseTemplates(DEFAULT_TEMPLATES);
|
||||
|
||||
@@ -4,8 +4,7 @@ import {
|
||||
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route,
|
||||
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree,
|
||||
} from 'app/features/alerting/unified/openapi/routesApi.gen';
|
||||
import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance';
|
||||
import { K8sAnnotations, ROOT_ROUTE_NAME } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
import { K8sAnnotations, PROVENANCE_NONE, ROOT_ROUTE_NAME } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
import { AlertManagerCortexConfig, MatcherOperator, Route } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
/**
|
||||
@@ -67,7 +66,7 @@ export const getUserDefinedRoutingTree: (
|
||||
name: ROOT_ROUTE_NAME,
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
[K8sAnnotations.Provenance]: KnownProvenance.None,
|
||||
[K8sAnnotations.Provenance]: PROVENANCE_NONE,
|
||||
},
|
||||
// Resource versions are much shorter than this in reality, but this is an easy way
|
||||
// for us to mock the concurrency logic and check if the policies have updated since the last fetch
|
||||
|
||||
@@ -6,9 +6,8 @@ import {
|
||||
} from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
|
||||
import { ALERTING_API_SERVER_BASE_URL, getK8sResponse } from 'app/features/alerting/unified/mocks/server/utils';
|
||||
import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver } from 'app/features/alerting/unified/openapi/receiversApi.gen';
|
||||
import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
|
||||
const usedByPolicies = ['grafana-default-email'];
|
||||
const usedByRules = ['grafana-default-email'];
|
||||
@@ -24,7 +23,7 @@ const getReceiversList = () => {
|
||||
const provenance =
|
||||
contactPoint.grafana_managed_receiver_configs?.find((integration) => {
|
||||
return integration.provenance;
|
||||
})?.provenance || KnownProvenance.None;
|
||||
})?.provenance || PROVENANCE_NONE;
|
||||
return {
|
||||
metadata: {
|
||||
// This isn't exactly accurate, but its the cleanest way to use the same data for AM config and K8S responses
|
||||
|
||||
@@ -3,9 +3,8 @@ import { HttpResponse, http } from 'msw';
|
||||
import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
|
||||
import { ALERTING_API_SERVER_BASE_URL, getK8sResponse } from 'app/features/alerting/unified/mocks/server/utils';
|
||||
import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroup } from 'app/features/alerting/unified/openapi/templatesApi.gen';
|
||||
import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { PROVENANCE_ANNOTATION } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
import { PROVENANCE_ANNOTATION, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
|
||||
const config = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME);
|
||||
|
||||
@@ -15,7 +14,7 @@ const mappedTemplates = Object.entries(
|
||||
).map<ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroup>(([title, template]) => ({
|
||||
metadata: {
|
||||
name: titleToK8sResourceName(title), // K8s uses unique identifiers for resources
|
||||
annotations: { [PROVENANCE_ANNOTATION]: config.template_file_provenances?.[title] || KnownProvenance.None },
|
||||
annotations: { [PROVENANCE_ANNOTATION]: config.template_file_provenances?.[title] || PROVENANCE_NONE },
|
||||
},
|
||||
spec: {
|
||||
title: title,
|
||||
|
||||
@@ -4,8 +4,7 @@ import { base64UrlEncode } from '@grafana/alerting';
|
||||
import { filterBySelector } from 'app/features/alerting/unified/mocks/server/handlers/k8s/utils';
|
||||
import { ALERTING_API_SERVER_BASE_URL, getK8sResponse } from 'app/features/alerting/unified/mocks/server/utils';
|
||||
import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval } from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen';
|
||||
import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance';
|
||||
import { K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
|
||||
/** UID of a time interval that we expect to follow all happy paths within tests/mocks */
|
||||
export const TIME_INTERVAL_UID_HAPPY_PATH = 'f4eae7a4895fa786';
|
||||
@@ -22,7 +21,7 @@ const allTimeIntervals = getK8sResponse<ComGithubGrafanaGrafanaPkgApisAlertingNo
|
||||
{
|
||||
metadata: {
|
||||
annotations: {
|
||||
[K8sAnnotations.Provenance]: KnownProvenance.None,
|
||||
[K8sAnnotations.Provenance]: PROVENANCE_NONE,
|
||||
},
|
||||
name: base64UrlEncode(TIME_INTERVAL_NAME_HAPPY_PATH),
|
||||
uid: TIME_INTERVAL_UID_HAPPY_PATH,
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export enum KnownProvenance {
|
||||
None = 'none' /** Provenance value given for entities that were not provisioned */,
|
||||
API = 'api',
|
||||
File = 'file',
|
||||
ConvertedPrometheus = 'converted_prometheus',
|
||||
}
|
||||
@@ -4,6 +4,9 @@
|
||||
* */
|
||||
export const PROVENANCE_ANNOTATION = 'grafana.com/provenance';
|
||||
|
||||
/** Value of {@link PROVENANCE_ANNOTATION} given for entities that were not provisioned */
|
||||
export const PROVENANCE_NONE = 'none';
|
||||
|
||||
export enum K8sAnnotations {
|
||||
Provenance = 'grafana.com/provenance',
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
|
||||
import { encodeFieldSelector, isProvisionedResource } from './utils';
|
||||
import { encodeFieldSelector } from './utils';
|
||||
|
||||
describe('encodeFieldSelector', () => {
|
||||
it('should escape backslashes', () => {
|
||||
@@ -27,29 +25,3 @@ describe('encodeFieldSelector', () => {
|
||||
expect(encodeFieldSelector('foo=bar,bar=baz,qux\\foo')).toBe('foo\\=bar\\,bar\\=baz\\,qux\\\\foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isProvisionedResource', () => {
|
||||
it('should return true when provenance is API', () => {
|
||||
expect(isProvisionedResource(KnownProvenance.API)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when provenance is File', () => {
|
||||
expect(isProvisionedResource(KnownProvenance.File)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when provenance is ConvertedPrometheus', () => {
|
||||
expect(isProvisionedResource(KnownProvenance.ConvertedPrometheus)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when provenance is none', () => {
|
||||
expect(isProvisionedResource(KnownProvenance.None)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when provenance is undefined', () => {
|
||||
expect(isProvisionedResource(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for any other non-empty string', () => {
|
||||
expect(isProvisionedResource('custom-provenance')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { IoK8SApimachineryPkgApisMetaV1ObjectMeta } from 'app/features/alerting/unified/openapi/receiversApi.gen';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
|
||||
/**
|
||||
* Should we call the kubernetes-style API for managing alertmanager entities?
|
||||
@@ -24,7 +22,7 @@ type EntityToCheck = {
|
||||
*/
|
||||
export const isK8sEntityProvisioned = (k8sEntity: EntityToCheck) => {
|
||||
const provenance = getAnnotation(k8sEntity, K8sAnnotations.Provenance);
|
||||
return isProvisionedResource(provenance);
|
||||
return Boolean(provenance && provenance !== PROVENANCE_NONE);
|
||||
};
|
||||
|
||||
export const ANNOTATION_PREFIX_ACCESS = 'grafana.com/access/';
|
||||
@@ -61,7 +59,3 @@ export const stringifyFieldSelector = (fieldSelectors: FieldSelector[]): string
|
||||
.map(([key, value, operator = '=']) => `${key}${operator}${encodeFieldSelector(value)}`)
|
||||
.join(',');
|
||||
};
|
||||
|
||||
export function isProvisionedResource(provenance?: string): boolean {
|
||||
return Boolean(provenance && provenance !== KnownProvenance.None);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('buildCategories', () => {
|
||||
it('should add enterprise phantom plugins', () => {
|
||||
const enterprisePluginsCategory = categories[3];
|
||||
expect(enterprisePluginsCategory.title).toBe('Enterprise plugins');
|
||||
expect(enterprisePluginsCategory.plugins.length).toBe(32);
|
||||
expect(enterprisePluginsCategory.plugins.length).toBe(31);
|
||||
expect(enterprisePluginsCategory.plugins[0].name).toBe('Adobe Analytics');
|
||||
expect(enterprisePluginsCategory.plugins[enterprisePluginsCategory.plugins.length - 1].name).toBe('Zendesk');
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@ import catchpointSvg from 'img/plugins/catchpoint.svg';
|
||||
import cloudflareJpg from 'img/plugins/cloudflare.jpg';
|
||||
import cockroachdbJpg from 'img/plugins/cockroachdb.jpg';
|
||||
import datadogPng from 'img/plugins/datadog.png';
|
||||
import db2Svg from 'img/plugins/db2.svg';
|
||||
import droneSvg from 'img/plugins/drone.svg';
|
||||
import dynatracePng from 'img/plugins/dynatrace.png';
|
||||
import gitlabSvg from 'img/plugins/gitlab.svg';
|
||||
@@ -419,12 +418,6 @@ function getEnterprisePhantomPlugins(): DataSourcePluginMeta[] {
|
||||
name: 'SolarWinds',
|
||||
imgUrl: solarWindsSvg,
|
||||
}),
|
||||
getPhantomPlugin({
|
||||
id: 'grafana-ibmdb2-datasource',
|
||||
description: t('datasources.get-enterprise-phantom-plugins.description.ibmdb2-datasource', 'IBM Db2 data source'),
|
||||
name: 'IBM Db2',
|
||||
imgUrl: db2Svg,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ const cloudwatchPlugin = async () =>
|
||||
await import(/* webpackChunkName: "cloudwatchPlugin" */ 'app/plugins/datasource/cloudwatch/module');
|
||||
const dashboardDSPlugin = async () =>
|
||||
await import(/* webpackChunkName "dashboardDSPlugin" */ 'app/plugins/datasource/dashboard/module');
|
||||
const elasticsearchPlugin = async () =>
|
||||
await import(/* webpackChunkName: "elasticsearchPlugin" */ 'app/plugins/datasource/elasticsearch/module');
|
||||
const grafanaPlugin = async () =>
|
||||
await import(/* webpackChunkName: "grafanaPlugin" */ 'app/plugins/datasource/grafana/module');
|
||||
const influxdbPlugin = async () =>
|
||||
@@ -73,6 +75,7 @@ const builtInPlugins: Record<string, System.Module | (() => Promise<System.Modul
|
||||
// datasources
|
||||
'core:plugin/cloudwatch': cloudwatchPlugin,
|
||||
'core:plugin/dashboard': dashboardDSPlugin,
|
||||
'core:plugin/elasticsearch': elasticsearchPlugin,
|
||||
'core:plugin/grafana': grafanaPlugin,
|
||||
'core:plugin/influxdb': influxdbPlugin,
|
||||
'core:plugin/mixed': mixedPlugin,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import {
|
||||
API_GROUP as DASHBOARD_API_GROUP,
|
||||
BASE_URL as v0alphaBaseURL,
|
||||
} from '@grafana/api-clients/rtkq/dashboard/v0alpha1';
|
||||
import { BASE_URL as v0alphaBaseURL } from '@grafana/api-clients/rtkq/dashboard/v0alpha1';
|
||||
import { generatedAPI as legacyUserAPI } from '@grafana/api-clients/rtkq/legacy/user';
|
||||
import { DataFrame, DataFrameView, getDisplayProcessor, SelectableValue, toDataFrame } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
@@ -88,11 +85,10 @@ export class UnifiedSearcher implements GrafanaSearcher {
|
||||
fieldSelector: `metadata.name=${name}`,
|
||||
})
|
||||
);
|
||||
const items = result.data.items;
|
||||
starsIds = items?.length
|
||||
? items[0].spec.resource.find(({ group, kind }) => group === DASHBOARD_API_GROUP && kind === 'Dashboard')
|
||||
?.names || []
|
||||
: [];
|
||||
starsIds =
|
||||
result.data.items?.[0].spec.resource.find(
|
||||
(info) => info.group === 'dashboard.grafana.app' && info.kind === 'Dashboard'
|
||||
)?.names || [];
|
||||
} else {
|
||||
starsIds = await dispatch(legacyUserAPI.endpoints.getStars.initiate()).unwrap();
|
||||
}
|
||||
@@ -335,7 +331,7 @@ export class UnifiedSearcher implements GrafanaSearcher {
|
||||
}
|
||||
|
||||
if (query.deleted) {
|
||||
uri = `${getAPIBaseURL(DASHBOARD_API_GROUP, 'v1beta1')}/dashboards/?labelSelector=grafana.app/get-trash=true`;
|
||||
uri = `${getAPIBaseURL('dashboard.grafana.app', 'v1beta1')}/dashboards/?labelSelector=grafana.app/get-trash=true`;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
@@ -2,20 +2,8 @@ import { css } from '@emotion/css';
|
||||
import { useId, useState } from 'react';
|
||||
|
||||
import { createTheme, GrafanaTheme2, NewThemeOptions } from '@grafana/data';
|
||||
import { NewThemeOptionsSchema } from '@grafana/data/internal';
|
||||
import aubergine from '@grafana/data/themes/definitions/aubergine.json';
|
||||
import debug from '@grafana/data/themes/definitions/debug.json';
|
||||
import desertbloom from '@grafana/data/themes/definitions/desertbloom.json';
|
||||
import gildedgrove from '@grafana/data/themes/definitions/gildedgrove.json';
|
||||
import gloom from '@grafana/data/themes/definitions/gloom.json';
|
||||
import mars from '@grafana/data/themes/definitions/mars.json';
|
||||
import matrix from '@grafana/data/themes/definitions/matrix.json';
|
||||
import sapphiredusk from '@grafana/data/themes/definitions/sapphiredusk.json';
|
||||
import synthwave from '@grafana/data/themes/definitions/synthwave.json';
|
||||
import tron from '@grafana/data/themes/definitions/tron.json';
|
||||
import victorian from '@grafana/data/themes/definitions/victorian.json';
|
||||
import zen from '@grafana/data/themes/definitions/zen.json';
|
||||
import themeJsonSchema from '@grafana/data/themes/schema.generated.json';
|
||||
import { experimentalThemeDefinitions, NewThemeOptionsSchema } from '@grafana/data/internal';
|
||||
import { themeJsonSchema } from '@grafana/data/unstable';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { useChromeHeaderHeight } from '@grafana/runtime';
|
||||
import { CodeEditor, Combobox, Field, Stack, useStyles2 } from '@grafana/ui';
|
||||
@@ -46,23 +34,8 @@ const themeMap: Record<string, NewThemeOptions> = {
|
||||
},
|
||||
};
|
||||
|
||||
const experimentalDefinitions: Record<string, unknown> = {
|
||||
aubergine,
|
||||
debug,
|
||||
desertbloom,
|
||||
gildedgrove,
|
||||
gloom,
|
||||
mars,
|
||||
matrix,
|
||||
sapphiredusk,
|
||||
synthwave,
|
||||
tron,
|
||||
victorian,
|
||||
zen,
|
||||
};
|
||||
|
||||
// Add additional themes
|
||||
for (const [name, json] of Object.entries(experimentalDefinitions)) {
|
||||
for (const [name, json] of Object.entries(experimentalThemeDefinitions)) {
|
||||
const result = NewThemeOptionsSchema.safeParse(json);
|
||||
if (!result.success) {
|
||||
console.error(`Invalid theme definition for theme ${name}: ${result.error.message}`);
|
||||
|
||||
@@ -108,7 +108,7 @@ export interface GrafanaManagedContactPoint {
|
||||
/** If parsed from k8s API, we'll have an ID property */
|
||||
id?: string;
|
||||
metadata?: IoK8SApimachineryPkgApisMetaV1ObjectMeta;
|
||||
provenance?: string;
|
||||
provisioned?: boolean;
|
||||
grafana_managed_receiver_configs?: GrafanaManagedReceiverConfig[];
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ export type Route = {
|
||||
provenance?: string;
|
||||
/** this is used to add additional metadata to the routes without interfering with original route definition (symbols aren't iterable) */
|
||||
[ROUTES_META_SYMBOL]?: {
|
||||
provenance?: string;
|
||||
provisioned?: boolean;
|
||||
resourceVersion?: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
+6
-25
@@ -1,10 +1,10 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data';
|
||||
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
||||
import { ConnectionConfig } from '@grafana/google-sdk';
|
||||
import { ConfigSection, DataSourceDescription } from '@grafana/plugin-ui';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { Divider, Field, Input, SecureSocksProxySettings, Stack } from '@grafana/ui';
|
||||
import { reportInteraction, config } from '@grafana/runtime';
|
||||
import { Divider, SecureSocksProxySettings } from '@grafana/ui';
|
||||
|
||||
import { CloudMonitoringOptions, CloudMonitoringSecureJsonData } from '../../types/types';
|
||||
|
||||
@@ -36,33 +36,14 @@ export const ConfigEditor = memo(({ options, onOptionsChange }: Props) => {
|
||||
<Divider />
|
||||
<ConfigSection
|
||||
title="Additional settings"
|
||||
description="Additional settings are optional settings that can be configured for more control over your data source. This includes Secure Socks Proxy and Universe Domain."
|
||||
description="Additional settings are optional settings that can be configured for more control over your data source. This includes Secure Socks Proxy."
|
||||
isCollapsible
|
||||
isInitiallyOpen={
|
||||
options.jsonData.enableSecureSocksProxy !== undefined || options.jsonData.universeDomain !== undefined
|
||||
}
|
||||
isInitiallyOpen={options.jsonData.enableSecureSocksProxy !== undefined}
|
||||
>
|
||||
<Stack direction={'column'}>
|
||||
<Field noMargin label="Universe Domain">
|
||||
<Input
|
||||
width={50}
|
||||
value={options.jsonData.universeDomain}
|
||||
onChange={(event) =>
|
||||
updateDatasourcePluginJsonDataOption(
|
||||
{ options, onOptionsChange },
|
||||
'universeDomain',
|
||||
event.currentTarget.value
|
||||
)
|
||||
}
|
||||
placeholder="googleapis.com"
|
||||
></Input>
|
||||
</Field>
|
||||
<SecureSocksProxySettings options={options} onOptionsChange={onOptionsChange} />
|
||||
</Stack>
|
||||
<SecureSocksProxySettings options={options} onOptionsChange={onOptionsChange} />
|
||||
</ConfigSection>
|
||||
</>
|
||||
)}
|
||||
<Divider />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -38,7 +38,6 @@ export interface Aggregation {
|
||||
export interface CloudMonitoringOptions extends DataSourceOptions {
|
||||
gceDefaultProject?: string;
|
||||
enableSecureSocksProxy?: boolean;
|
||||
universeDomain?: string;
|
||||
}
|
||||
|
||||
export interface CloudMonitoringSecureJsonData extends DataSourceSecureJsonData {}
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { select } from 'react-select-event';
|
||||
|
||||
import { DateHistogram } from '../../../../dataquery.gen';
|
||||
import { DateHistogram } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
||||
|
||||
import { DateHistogramSettingsEditor } from './DateHistogramSettingsEditor';
|
||||
|
||||
+3
-3
@@ -4,9 +4,9 @@ import { GroupBase, OptionsOrGroups } from 'react-select';
|
||||
|
||||
import { InternalTimeZones, SelectableValue } from '@grafana/data';
|
||||
import { InlineField, Input, Select, TimeZonePicker } from '@grafana/ui';
|
||||
import { DateHistogram } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { calendarIntervals } from '../../../../QueryBuilder';
|
||||
import { DateHistogram } from '../../../../dataquery.gen';
|
||||
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
||||
import { useCreatableSelectPersistedBehaviour } from '../../../hooks/useCreatableSelectPersistedBehaviour';
|
||||
import { changeBucketAggregationSetting } from '../state/actions';
|
||||
@@ -37,11 +37,11 @@ const hasValue =
|
||||
const isValidNewOption = (
|
||||
inputValue: string,
|
||||
_: SelectableValue<string> | null,
|
||||
options: OptionsOrGroups<SelectableValue<string>, GroupBase<SelectableValue<string>>>
|
||||
options: OptionsOrGroups<unknown, GroupBase<unknown>>
|
||||
) => {
|
||||
// TODO: would be extremely nice here to allow only template variables and values that are
|
||||
// valid date histogram's Interval options
|
||||
const valueExists = options.some(hasValue(inputValue));
|
||||
const valueExists = (options as Array<SelectableValue<string>>).some(hasValue(inputValue));
|
||||
// we also don't want users to create "empty" values
|
||||
return !valueExists && inputValue.trim().length > 0;
|
||||
};
|
||||
|
||||
+1
-1
@@ -3,8 +3,8 @@ import { uniqueId } from 'lodash';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { InlineField, Input, QueryField } from '@grafana/ui';
|
||||
import { Filters } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { Filters } from '../../../../../dataquery.gen';
|
||||
import { useDispatch, useStatelessReducer } from '../../../../../hooks/useStatelessReducer';
|
||||
import { AddRemove } from '../../../../AddRemove';
|
||||
import { changeBucketAggregationSetting } from '../../state/actions';
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { Filter } from '../../../../../../dataquery.gen';
|
||||
import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
export const addFilter = createAction('@bucketAggregations/filter/add');
|
||||
export const removeFilter = createAction<number>('@bucketAggregations/filter/remove');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user