Compare commits
16 Commits
dual-write
...
ifrost/tra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c558072c0c | ||
|
|
2ce89f099f | ||
|
|
48625d67e5 | ||
|
|
8bad33de4c | ||
|
|
040854c8af | ||
|
|
829022d488 | ||
|
|
987c1fc6b6 | ||
|
|
170ac31c5a | ||
|
|
0d1e0bc21c | ||
|
|
afd84f0335 | ||
|
|
d680537ea1 | ||
|
|
78d507d285 | ||
|
|
9d1d0e72c2 | ||
|
|
fd955f90ac | ||
|
|
ccb032f376 | ||
|
|
cf452c167b |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -543,6 +543,7 @@ 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
|
||||
|
||||
@@ -28,7 +28,7 @@ type check struct {
|
||||
PluginStore pluginstore.Store
|
||||
PluginContextProvider PluginContextProvider
|
||||
PluginClient plugins.Client
|
||||
PluginRepo repo.Service
|
||||
PluginRepo checks.PluginInfoGetter
|
||||
GrafanaVersion string
|
||||
pluginCanBeInstalledCache map[string]bool
|
||||
pluginExistsCacheMu sync.RWMutex
|
||||
@@ -39,7 +39,7 @@ func New(
|
||||
pluginStore pluginstore.Store,
|
||||
pluginContextProvider PluginContextProvider,
|
||||
pluginClient plugins.Client,
|
||||
pluginRepo repo.Service,
|
||||
pluginRepo checks.PluginInfoGetter,
|
||||
grafanaVersion string,
|
||||
) checks.Check {
|
||||
return &check{
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
type missingPluginStep struct {
|
||||
PluginStore pluginstore.Store
|
||||
PluginRepo repo.Service
|
||||
PluginRepo checks.PluginInfoGetter
|
||||
GrafanaVersion string
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/plugins/repo"
|
||||
)
|
||||
|
||||
// Check returns metadata about the check being executed and the list of Steps
|
||||
@@ -37,3 +38,10 @@ type Step interface {
|
||||
// Run executes the step for an item and returns a report
|
||||
Run(ctx context.Context, log logging.Logger, obj *advisorv0alpha1.CheckSpec, item any) ([]advisorv0alpha1.CheckReportFailure, error)
|
||||
}
|
||||
|
||||
// PluginInfoGetter is a minimal interface for retrieving plugin information from a repository.
|
||||
// It contains only the GetPluginsInfo method used by plugincheck and datasourcecheck.
|
||||
type PluginInfoGetter interface {
|
||||
// GetPluginsInfo will return a list of plugins from grafana.com/api/plugins.
|
||||
GetPluginsInfo(ctx context.Context, options repo.GetPluginsInfoOptions, compatOpts repo.CompatOpts) ([]repo.PluginInfo, error)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const (
|
||||
|
||||
func New(
|
||||
pluginStore pluginstore.Store,
|
||||
pluginRepo repo.Service,
|
||||
pluginRepo checks.PluginInfoGetter,
|
||||
updateChecker pluginchecker.PluginUpdateChecker,
|
||||
pluginErrorResolver plugins.ErrorResolver,
|
||||
grafanaVersion string,
|
||||
@@ -33,7 +33,7 @@ func New(
|
||||
|
||||
type check struct {
|
||||
PluginStore pluginstore.Store
|
||||
PluginRepo repo.Service
|
||||
PluginRepo checks.PluginInfoGetter
|
||||
updateChecker pluginchecker.PluginUpdateChecker
|
||||
pluginErrorResolver plugins.ErrorResolver
|
||||
GrafanaVersion string
|
||||
|
||||
@@ -83,6 +83,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
|
||||
| `reportingRetries` | Enables rendering retries for the reporting feature |
|
||||
| `externalServiceAccounts` | Automatic service account and token setup for plugins |
|
||||
| `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches |
|
||||
| `dashboardNewLayouts` | Enables new dashboard layouts |
|
||||
| `pdfTables` | Enables generating table data as PDF in reporting |
|
||||
| `canvasPanelPanZoom` | Allow pan and zoom in canvas panel |
|
||||
| `alertingSaveStateCompressed` | Enables the compressed protobuf-based alert state storage. Default is enabled. |
|
||||
|
||||
@@ -30,7 +30,9 @@ refs:
|
||||
|
||||
# Datagrid
|
||||
|
||||
{{< docs/experimental product="The datagrid visualization" featureFlag="`enableDatagridEditing`" >}}
|
||||
{{< admonition type="caution" >}}
|
||||
Starting with Grafana 12.4, Datagrid is deprecated. It will be removed in version 13.0.
|
||||
{{< /admonition >}}
|
||||
|
||||
Datagrids offer you the ability to create, edit, and fine-tune data within Grafana. As such, this panel can act as a data source for other panels
|
||||
inside a dashboard.
|
||||
|
||||
23
go.mod
23
go.mod
@@ -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.0 // @grafana/grafana-search-and-storage
|
||||
github.com/blevesearch/bleve_index_api v1.2.7 // @grafana/grafana-search-and-storage
|
||||
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/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.1.20 // indirect
|
||||
github.com/blevesearch/go-faiss v1.0.25 // indirect
|
||||
github.com/blevesearch/geo v0.2.4 // indirect
|
||||
github.com/blevesearch/go-faiss v1.0.26 // 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.9 // indirect
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.13 // 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.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/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/bluele/gcache v0.0.2 // indirect
|
||||
github.com/blugelabs/ice v1.0.0 // indirect
|
||||
github.com/blugelabs/ice/v2 v2.0.1 // indirect
|
||||
@@ -443,7 +443,6 @@ 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
|
||||
|
||||
46
go.sum
46
go.sum
@@ -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.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/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/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.9 h1:X6nJXnNHl7nasXW+U6y2Ns2Aw8F9STszkYkyBfQ+p0o=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.9/go.mod h1:IrzspZlVjhf4X29oJiEhBxEteTqOY9RlYlk1lCmYHr4=
|
||||
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/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.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/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/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,8 +1442,6 @@ 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=
|
||||
|
||||
29
go.work.sum
29
go.work.sum
@@ -520,14 +520,40 @@ 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=
|
||||
@@ -998,8 +1024,6 @@ 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=
|
||||
@@ -1092,6 +1116,7 @@ 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=
|
||||
|
||||
@@ -293,8 +293,8 @@
|
||||
"@grafana/plugin-ui": "^0.11.1",
|
||||
"@grafana/prometheus": "workspace:*",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/scenes": "v6.52.1",
|
||||
"@grafana/scenes-react": "v6.52.1",
|
||||
"@grafana/scenes": "6.52.2",
|
||||
"@grafana/scenes-react": "6.52.2",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/sql": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
|
||||
@@ -35,6 +35,14 @@
|
||||
},
|
||||
"./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": {
|
||||
@@ -52,7 +60,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 ./src/themes/scripts/generateSchema.ts"
|
||||
"themes-schema": "tsx ./scripts/generateSchema.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "7.0.1",
|
||||
@@ -102,6 +110,7 @@
|
||||
"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,21 +1,40 @@
|
||||
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: [...plugins, json()],
|
||||
plugins: grafanaDataPlugins,
|
||||
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
|
||||
treeshake: false,
|
||||
},
|
||||
{
|
||||
input: 'src/unstable.ts',
|
||||
plugins: [...plugins, json()],
|
||||
plugins: grafanaDataPlugins,
|
||||
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
|
||||
treeshake: false,
|
||||
},
|
||||
|
||||
22
packages/grafana-data/scripts/generateSchema.ts
Normal file
22
packages/grafana-data/scripts/generateSchema.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
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');
|
||||
@@ -844,7 +844,6 @@ export {
|
||||
DataLinkConfigOrigin,
|
||||
SupportedTransformationType,
|
||||
type InternalDataLink,
|
||||
type LinkTarget,
|
||||
type LinkModel,
|
||||
type LinkModelSupplier,
|
||||
VariableOrigin,
|
||||
@@ -852,6 +851,7 @@ export {
|
||||
VariableSuggestionsScope,
|
||||
OneClickMode,
|
||||
} from './types/dataLink';
|
||||
export { type LinkTarget } from './types/linkTarget';
|
||||
export {
|
||||
type Action,
|
||||
type ActionModel,
|
||||
|
||||
@@ -93,7 +93,6 @@ 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,7 +1,18 @@
|
||||
import { Registry, RegistryItem } from '../utils/Registry';
|
||||
|
||||
import { createTheme, NewThemeOptionsSchema } from './createTheme';
|
||||
import * as extraThemes from './themeDefinitions';
|
||||
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 { GrafanaTheme2 } from './types';
|
||||
|
||||
export interface ThemeRegistryItem extends RegistryItem {
|
||||
@@ -9,6 +20,21 @@ 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
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
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
|
||||
)
|
||||
);
|
||||
@@ -1,12 +0,0 @@
|
||||
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';
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ScopedVars } from './ScopedVars';
|
||||
import { ExploreCorrelationHelperData, ExplorePanelsState } from './explore';
|
||||
import { LinkTarget } from './linkTarget';
|
||||
import { InterpolateFunction } from './panel';
|
||||
import { DataQuery } from './query';
|
||||
import { TimeRange } from './time';
|
||||
@@ -88,8 +89,6 @@ export interface InternalDataLink<T extends DataQuery = any> {
|
||||
range?: TimeRange;
|
||||
}
|
||||
|
||||
export type LinkTarget = '_blank' | '_self' | undefined;
|
||||
|
||||
/**
|
||||
* Processed Link Model. The values are ready to use
|
||||
*/
|
||||
|
||||
@@ -356,7 +356,7 @@ export interface FeatureToggles {
|
||||
*/
|
||||
dashboardScene?: boolean;
|
||||
/**
|
||||
* Enables experimental new dashboard layouts
|
||||
* Enables new dashboard layouts
|
||||
*/
|
||||
dashboardNewLayouts?: boolean;
|
||||
/**
|
||||
@@ -531,6 +531,10 @@ export interface FeatureToggles {
|
||||
*/
|
||||
alertingListViewV2?: boolean;
|
||||
/**
|
||||
* Enables the new Alerting navigation structure with improved menu grouping
|
||||
*/
|
||||
alertingNavigationV2?: boolean;
|
||||
/**
|
||||
* Enables saved searches for alert rules list
|
||||
*/
|
||||
alertingSavedSearches?: boolean;
|
||||
@@ -1251,4 +1255,8 @@ export interface FeatureToggles {
|
||||
* Enables profiles exemplars support in profiles drilldown
|
||||
*/
|
||||
profilesExemplars?: boolean;
|
||||
/**
|
||||
* Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods
|
||||
*/
|
||||
alertingSyncDispatchTimer?: boolean;
|
||||
}
|
||||
|
||||
4
packages/grafana-data/src/types/linkTarget.ts
Normal file
4
packages/grafana-data/src/types/linkTarget.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Target for links - controls whether link opens in new tab or same tab
|
||||
*/
|
||||
export type LinkTarget = '_blank' | '_self' | undefined;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
import { LinkTarget } from './dataLink';
|
||||
import { IconName } from './icon';
|
||||
import { LinkTarget } from './linkTarget';
|
||||
|
||||
export interface NavLinkDTO {
|
||||
id?: string;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { DataFrame } from './dataFrame';
|
||||
import { DataQueryError, DataQueryRequest, DataQueryTimings } from './datasource';
|
||||
import { FieldConfigSource } from './fieldOverrides';
|
||||
import { IconName } from './icon';
|
||||
import { LinkTarget } from './linkTarget';
|
||||
import { OptionEditorConfig } from './options';
|
||||
import { PluginMeta } from './plugin';
|
||||
import { AbsoluteTimeRange, TimeRange, TimeZone } from './time';
|
||||
@@ -191,6 +192,7 @@ export interface PanelMenuItem {
|
||||
onClick?: (event: React.MouseEvent) => void;
|
||||
shortcut?: string;
|
||||
href?: string;
|
||||
target?: LinkTarget;
|
||||
subMenu?: PanelMenuItem[];
|
||||
}
|
||||
|
||||
|
||||
@@ -9,4 +9,4 @@
|
||||
* and be subject to the standard policies
|
||||
*/
|
||||
|
||||
export { default as themeJsonSchema } from './themes/schema.generated.json';
|
||||
export {};
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"emitDeclarationOnly": true,
|
||||
"isolatedModules": true,
|
||||
"rootDirs": ["."],
|
||||
"moduleResolution": "bundler"
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": ["dist/**/*"],
|
||||
"include": [
|
||||
|
||||
@@ -574,8 +574,8 @@ var (
|
||||
},
|
||||
{
|
||||
Name: "dashboardNewLayouts",
|
||||
Description: "Enables experimental new dashboard layouts",
|
||||
Stage: FeatureStageExperimental,
|
||||
Description: "Enables new dashboard layouts",
|
||||
Stage: FeatureStagePublicPreview,
|
||||
FrontendOnly: false, // The restore backend feature changes behavior based on this flag
|
||||
Owner: grafanaDashboardsSquad,
|
||||
},
|
||||
@@ -879,6 +879,13 @@ var (
|
||||
Owner: grafanaAlertingSquad,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "alertingNavigationV2",
|
||||
Description: "Enables the new Alerting navigation structure with improved menu grouping",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaAlertingSquad,
|
||||
FrontendOnly: false,
|
||||
},
|
||||
{
|
||||
Name: "alertingSavedSearches",
|
||||
Description: "Enables saved searches for alert rules list",
|
||||
@@ -981,7 +988,8 @@ var (
|
||||
Stage: FeatureStageDeprecated,
|
||||
Owner: grafanaPartnerPluginsSquad,
|
||||
Expression: "true", // Enabled by default for now
|
||||
}, {
|
||||
},
|
||||
{
|
||||
Name: "alertingFilterV2",
|
||||
Description: "Enable the new alerting search experience",
|
||||
Stage: FeatureStageExperimental,
|
||||
@@ -2069,6 +2077,14 @@ var (
|
||||
Owner: grafanaObservabilityTracesAndProfilingSquad,
|
||||
FrontendOnly: false,
|
||||
},
|
||||
{
|
||||
Name: "alertingSyncDispatchTimer",
|
||||
Description: "Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaAlertingSquad,
|
||||
RequiresRestart: true,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
4
pkg/services/featuremgmt/toggles_gen.csv
generated
4
pkg/services/featuremgmt/toggles_gen.csv
generated
@@ -79,7 +79,7 @@ annotationPermissionUpdate,GA,@grafana/identity-access-team,false,false,false
|
||||
dashboardSceneForViewers,GA,@grafana/dashboards-squad,false,false,true
|
||||
dashboardSceneSolo,GA,@grafana/dashboards-squad,false,false,true
|
||||
dashboardScene,GA,@grafana/dashboards-squad,false,false,true
|
||||
dashboardNewLayouts,experimental,@grafana/dashboards-squad,false,false,false
|
||||
dashboardNewLayouts,preview,@grafana/dashboards-squad,false,false,false
|
||||
dashboardUndoRedo,experimental,@grafana/dashboards-squad,false,false,true
|
||||
unlimitedLayoutsNesting,experimental,@grafana/dashboards-squad,false,false,true
|
||||
drilldownRecommendations,experimental,@grafana/dashboards-squad,false,false,true
|
||||
@@ -121,6 +121,7 @@ dashboardLibrary,experimental,@grafana/sharing-squad,false,false,false
|
||||
suggestedDashboards,experimental,@grafana/sharing-squad,false,false,false
|
||||
dashboardTemplates,preview,@grafana/sharing-squad,false,false,false
|
||||
alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true
|
||||
alertingNavigationV2,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertingSavedSearches,experimental,@grafana/alerting-squad,false,false,true
|
||||
alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false
|
||||
preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-squad,false,false,false
|
||||
@@ -280,3 +281,4 @@ multiPropsVariables,experimental,@grafana/dashboards-squad,false,false,true
|
||||
smoothingTransformation,experimental,@grafana/datapro,false,false,true
|
||||
secretsManagementAppPlatformAwsKeeper,experimental,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
profilesExemplars,experimental,@grafana/observability-traces-and-profiling,false,false,false
|
||||
alertingSyncDispatchTimer,experimental,@grafana/alerting-squad,false,true,false
|
||||
|
||||
|
10
pkg/services/featuremgmt/toggles_gen.go
generated
10
pkg/services/featuremgmt/toggles_gen.go
generated
@@ -260,7 +260,7 @@ const (
|
||||
FlagAnnotationPermissionUpdate = "annotationPermissionUpdate"
|
||||
|
||||
// FlagDashboardNewLayouts
|
||||
// Enables experimental new dashboard layouts
|
||||
// Enables new dashboard layouts
|
||||
FlagDashboardNewLayouts = "dashboardNewLayouts"
|
||||
|
||||
// FlagPdfTables
|
||||
@@ -371,6 +371,10 @@ const (
|
||||
// Enables a flow to get started with a new dashboard from a template
|
||||
FlagDashboardTemplates = "dashboardTemplates"
|
||||
|
||||
// FlagAlertingNavigationV2
|
||||
// Enables the new Alerting navigation structure with improved menu grouping
|
||||
FlagAlertingNavigationV2 = "alertingNavigationV2"
|
||||
|
||||
// FlagAlertingDisableSendAlertsExternal
|
||||
// Disables the ability to send alerts to an external Alertmanager datasource.
|
||||
FlagAlertingDisableSendAlertsExternal = "alertingDisableSendAlertsExternal"
|
||||
@@ -789,4 +793,8 @@ const (
|
||||
// FlagProfilesExemplars
|
||||
// Enables profiles exemplars support in profiles drilldown
|
||||
FlagProfilesExemplars = "profilesExemplars"
|
||||
|
||||
// FlagAlertingSyncDispatchTimer
|
||||
// Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods
|
||||
FlagAlertingSyncDispatchTimer = "alertingSyncDispatchTimer"
|
||||
)
|
||||
|
||||
40
pkg/services/featuremgmt/toggles_gen.json
generated
40
pkg/services/featuremgmt/toggles_gen.json
generated
@@ -348,6 +348,18 @@
|
||||
"expression": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingNavigationV2",
|
||||
"resourceVersion": "1768320918269",
|
||||
"creationTimestamp": "2026-01-13T16:15:18Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables the new Alerting navigation structure with improved menu grouping",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/alerting-squad"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingNotificationHistory",
|
||||
@@ -511,6 +523,20 @@
|
||||
"frontend": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingSyncDispatchTimer",
|
||||
"resourceVersion": "1766161788928",
|
||||
"creationTimestamp": "2025-12-19T16:29:48Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/alerting-squad",
|
||||
"requiresRestart": true,
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingTriage",
|
||||
@@ -662,7 +688,8 @@
|
||||
"metadata": {
|
||||
"name": "auditLoggingAppPlatform",
|
||||
"resourceVersion": "1767013056996",
|
||||
"creationTimestamp": "2025-12-29T12:57:36Z"
|
||||
"creationTimestamp": "2025-12-29T12:57:36Z",
|
||||
"deletionTimestamp": "2026-01-06T09:18:36Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enable audit logging with Kubernetes under app platform",
|
||||
@@ -1015,12 +1042,15 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "dashboardNewLayouts",
|
||||
"resourceVersion": "1764664939750",
|
||||
"creationTimestamp": "2024-10-23T08:55:45Z"
|
||||
"resourceVersion": "1768382835527",
|
||||
"creationTimestamp": "2024-10-23T08:55:45Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2026-01-14 09:27:15.527103 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables experimental new dashboard layouts",
|
||||
"stage": "experimental",
|
||||
"description": "Enables new dashboard layouts",
|
||||
"stage": "preview",
|
||||
"codeowner": "@grafana/dashboards-squad"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -54,8 +54,7 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
|
||||
}
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if c.HasRole(identity.RoleAdmin) &&
|
||||
(s.cfg.StackID == "" || // show OnPrem even when provisioning is disabled
|
||||
s.features.IsEnabledGlobally(featuremgmt.FlagProvisioning)) {
|
||||
s.features.IsEnabledGlobally(featuremgmt.FlagProvisioning) {
|
||||
generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
|
||||
Text: "Provisioning",
|
||||
Id: "provisioning",
|
||||
|
||||
@@ -213,6 +213,9 @@ func (ng *AlertNG) init() error {
|
||||
SkipVerify: ng.Cfg.Smtp.SkipVerify,
|
||||
StaticHeaders: ng.Cfg.Smtp.StaticHeaders,
|
||||
}
|
||||
runtimeConfig := remoteClient.RuntimeConfig{
|
||||
DispatchTimer: notifier.GetDispatchTimer(ng.FeatureToggles).String(),
|
||||
}
|
||||
|
||||
cfg := remote.AlertmanagerConfig{
|
||||
BasicAuthPassword: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.Password,
|
||||
@@ -222,6 +225,7 @@ func (ng *AlertNG) init() error {
|
||||
ExternalURL: ng.Cfg.AppURL,
|
||||
SmtpConfig: smtpCfg,
|
||||
Timeout: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.Timeout,
|
||||
RuntimeConfig: runtimeConfig,
|
||||
}
|
||||
autogenFn := func(ctx context.Context, logger log.Logger, orgID int64, cfg *definitions.PostableApiAlertingConfig, invalidReceiverAction notifier.InvalidReceiversAction) error {
|
||||
return notifier.AddAutogenConfig(ctx, logger, ng.store, orgID, cfg, invalidReceiverAction, ng.FeatureToggles)
|
||||
|
||||
@@ -33,6 +33,9 @@ const (
|
||||
|
||||
// How long we keep silences in the kvstore after they've expired.
|
||||
silenceRetention = 5 * 24 * time.Hour
|
||||
|
||||
// How long we keep flushes in the kvstore after they've expired.
|
||||
flushRetention = 5 * 24 * time.Hour
|
||||
)
|
||||
|
||||
type AlertingStore interface {
|
||||
@@ -44,8 +47,10 @@ type AlertingStore interface {
|
||||
type stateStore interface {
|
||||
SaveSilences(ctx context.Context, st alertingNotify.State) (int64, error)
|
||||
SaveNotificationLog(ctx context.Context, st alertingNotify.State) (int64, error)
|
||||
SaveFlushLog(ctx context.Context, st alertingNotify.State) (int64, error)
|
||||
GetSilences(ctx context.Context) (string, error)
|
||||
GetNotificationLog(ctx context.Context) (string, error)
|
||||
GetFlushLog(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
type alertmanager struct {
|
||||
@@ -101,6 +106,10 @@ func NewAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
flushLog, err := stateStore.GetFlushLog(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
silencesOptions := maintenanceOptions{
|
||||
initialState: silences,
|
||||
@@ -123,12 +132,29 @@ func NewAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A
|
||||
}
|
||||
l := log.New("ngalert.notifier")
|
||||
|
||||
dispatchTimer := GetDispatchTimer(featureToggles)
|
||||
|
||||
var flushLogOptions *maintenanceOptions
|
||||
if dispatchTimer == alertingNotify.DispatchTimerSync {
|
||||
flushLogOptions = &maintenanceOptions{
|
||||
initialState: flushLog,
|
||||
retention: flushRetention,
|
||||
maintenanceFrequency: maintenanceInterval,
|
||||
maintenanceFunc: func(state alertingNotify.State) (int64, error) {
|
||||
// Detached context here is to make sure that when the service is shut down the persist operation is executed.
|
||||
return stateStore.SaveFlushLog(context.Background(), state)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
opts := alertingNotify.GrafanaAlertmanagerOpts{
|
||||
ExternalURL: cfg.AppURL,
|
||||
AlertStoreCallback: nil,
|
||||
PeerTimeout: cfg.UnifiedAlerting.HAPeerTimeout,
|
||||
Silences: silencesOptions,
|
||||
Nflog: nflogOptions,
|
||||
FlushLog: flushLogOptions,
|
||||
DispatchTimer: dispatchTimer,
|
||||
Limits: alertingNotify.Limits{
|
||||
MaxSilences: cfg.UnifiedAlerting.AlertmanagerMaxSilencesCount,
|
||||
MaxSilenceSizeBytes: cfg.UnifiedAlerting.AlertmanagerMaxSilenceSizeBytes,
|
||||
|
||||
16
pkg/services/ngalert/notifier/dispatch_timer.go
Normal file
16
pkg/services/ngalert/notifier/dispatch_timer.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
alertingNotify "github.com/grafana/alerting/notify"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
// GetDispatchTimer returns the appropriate dispatch timer based on feature toggles.
|
||||
func GetDispatchTimer(features featuremgmt.FeatureToggles) (dt alertingNotify.DispatchTimer) {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
enabled := features.IsEnabledGlobally(featuremgmt.FlagAlertingSyncDispatchTimer)
|
||||
if enabled {
|
||||
dt = alertingNotify.DispatchTimerSync
|
||||
}
|
||||
return
|
||||
}
|
||||
36
pkg/services/ngalert/notifier/dispatch_timer_test.go
Normal file
36
pkg/services/ngalert/notifier/dispatch_timer_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
alertingNotify "github.com/grafana/alerting/notify"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetDispatchTimer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
featureFlagValue bool
|
||||
expected alertingNotify.DispatchTimer
|
||||
}{
|
||||
{
|
||||
name: "feature flag enabled returns sync timer",
|
||||
featureFlagValue: true,
|
||||
expected: alertingNotify.DispatchTimerSync,
|
||||
},
|
||||
{
|
||||
name: "feature flag disabled returns default timer",
|
||||
featureFlagValue: false,
|
||||
expected: alertingNotify.DispatchTimerDefault,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
features := featuremgmt.WithFeatures(featuremgmt.FlagAlertingSyncDispatchTimer, tt.featureFlagValue)
|
||||
result := GetDispatchTimer(features)
|
||||
require.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ const (
|
||||
KVNamespace = "alertmanager"
|
||||
NotificationLogFilename = "notifications"
|
||||
SilencesFilename = "silences"
|
||||
FlushLogFilename = "flushes"
|
||||
)
|
||||
|
||||
// FileStore is in charge of persisting the alertmanager files to the database.
|
||||
@@ -42,6 +43,10 @@ func (fileStore *FileStore) GetNotificationLog(ctx context.Context) (string, err
|
||||
return fileStore.contentFor(ctx, NotificationLogFilename)
|
||||
}
|
||||
|
||||
func (fileStore *FileStore) GetFlushLog(ctx context.Context) (string, error) {
|
||||
return fileStore.contentFor(ctx, FlushLogFilename)
|
||||
}
|
||||
|
||||
// contentFor returns the content for the given Alertmanager kvstore key.
|
||||
func (fileStore *FileStore) contentFor(ctx context.Context, filename string) (string, error) {
|
||||
// Then, let's attempt to read it from the database.
|
||||
@@ -74,6 +79,11 @@ func (fileStore *FileStore) SaveNotificationLog(ctx context.Context, st alerting
|
||||
return fileStore.persist(ctx, NotificationLogFilename, st)
|
||||
}
|
||||
|
||||
// SaveFlushLog saves the flush log to the database and returns the size of the unencoded state.
|
||||
func (fileStore *FileStore) SaveFlushLog(ctx context.Context, st alertingNotify.State) (int64, error) {
|
||||
return fileStore.persist(ctx, FlushLogFilename, st)
|
||||
}
|
||||
|
||||
// persist takes care of persisting the binary representation of internal state to the database as a base64 encoded string.
|
||||
func (fileStore *FileStore) persist(ctx context.Context, filename string, st alertingNotify.State) (int64, error) {
|
||||
var size int64
|
||||
|
||||
@@ -106,3 +106,48 @@ func TestFileStore_NotificationLog(t *testing.T) {
|
||||
t.Errorf("Unexpected Diff: %v", cmp.Diff(newState, decoded))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileStore_FlushLog(t *testing.T) {
|
||||
store := fakes.NewFakeKVStore(t)
|
||||
ctx := context.Background()
|
||||
var orgId int64 = 1
|
||||
|
||||
// Initialize kvstore with empty flush log state.
|
||||
initialState := flushLogState{} // FlushLog uses the same structure as nflog
|
||||
decodedState, err := initialState.MarshalBinary()
|
||||
require.NoError(t, err)
|
||||
encodedState := base64.StdEncoding.EncodeToString(decodedState)
|
||||
err = store.Set(ctx, orgId, KVNamespace, FlushLogFilename, encodedState)
|
||||
require.NoError(t, err)
|
||||
|
||||
fs := NewFileStore(orgId, store)
|
||||
|
||||
// Load initial (empty).
|
||||
flushLog, err := fs.GetFlushLog(ctx)
|
||||
require.NoError(t, err)
|
||||
decoded, err := decodeFlushLogState(strings.NewReader(flushLog))
|
||||
require.NoError(t, err)
|
||||
if !cmp.Equal(initialState, decoded) {
|
||||
t.Errorf("Unexpected Diff: %v", cmp.Diff(initialState, decoded))
|
||||
}
|
||||
|
||||
// Save new flush log state.
|
||||
now := time.Now()
|
||||
oneHour := now.Add(time.Hour)
|
||||
|
||||
v1 := createFlushLog(1, now, oneHour)
|
||||
v2 := createFlushLog(2, now, oneHour)
|
||||
newState := flushLogState{1: v1, 2: v2}
|
||||
size, err := fs.SaveFlushLog(ctx, newState)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, size, int64(0))
|
||||
|
||||
// Load new.
|
||||
flushLog, err = fs.GetFlushLog(ctx)
|
||||
require.NoError(t, err)
|
||||
decoded, err = decodeFlushLogState(strings.NewReader(flushLog))
|
||||
require.NoError(t, err)
|
||||
if !cmp.Equal(newState, decoded) {
|
||||
t.Errorf("Unexpected Diff: %v", cmp.Diff(newState, decoded))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ type Alertmanager interface {
|
||||
type ExternalState struct {
|
||||
Silences []byte
|
||||
Nflog []byte
|
||||
FlushLog []byte
|
||||
}
|
||||
|
||||
// StateMerger describes a type that is able to merge external state (nflog, silences) with its own.
|
||||
@@ -378,7 +379,7 @@ func (moa *MultiOrgAlertmanager) SyncAlertmanagersForOrgs(ctx context.Context, o
|
||||
func (moa *MultiOrgAlertmanager) cleanupOrphanLocalOrgState(ctx context.Context,
|
||||
activeOrganizations map[int64]struct{},
|
||||
) {
|
||||
storedFiles := []string{NotificationLogFilename, SilencesFilename}
|
||||
storedFiles := []string{NotificationLogFilename, SilencesFilename, FlushLogFilename}
|
||||
for _, fileName := range storedFiles {
|
||||
keys, err := moa.kvStore.Keys(ctx, kvstore.AllOrganizations, KVNamespace, fileName)
|
||||
if err != nil {
|
||||
|
||||
@@ -5,5 +5,8 @@ func (am *alertmanager) MergeState(state ExternalState) error {
|
||||
if err := am.Base.MergeNflog(state.Nflog); err != nil {
|
||||
return err
|
||||
}
|
||||
return am.Base.MergeSilences(state.Silences)
|
||||
if err := am.Base.MergeSilences(state.Silences); err != nil {
|
||||
return err
|
||||
}
|
||||
return am.Base.MergeFlushLog(state.FlushLog)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/matttproud/golang_protobuf_extensions/pbutil"
|
||||
"github.com/prometheus/alertmanager/flushlog/flushlogpb"
|
||||
"github.com/prometheus/alertmanager/nflog/nflogpb"
|
||||
"github.com/prometheus/alertmanager/silence/silencepb"
|
||||
"github.com/prometheus/common/model"
|
||||
@@ -228,15 +229,13 @@ func (f *FakeOrgStore) FetchOrgIds(_ context.Context) ([]int64, error) {
|
||||
return f.orgs, nil
|
||||
}
|
||||
|
||||
type NoValidation struct {
|
||||
}
|
||||
type NoValidation struct{}
|
||||
|
||||
func (n NoValidation) Validate(_ models.NotificationSettings) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type RejectingValidation struct {
|
||||
}
|
||||
type RejectingValidation struct{}
|
||||
|
||||
func (n RejectingValidation) Validate(s models.NotificationSettings) error {
|
||||
return ErrorReceiverDoesNotExist{ErrorReferenceInvalid: ErrorReferenceInvalid{Reference: s.Receiver}}
|
||||
@@ -365,6 +364,51 @@ func createNotificationLog(groupKey string, receiverName string, sentAt, expires
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/grafana/prometheus-alertmanager/blob/main/flushlog/flushlog.go#L136-L136
|
||||
type flushLogState map[uint64]*flushlogpb.MeshFlushLog
|
||||
|
||||
func (s flushLogState) MarshalBinary() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
for _, e := range s {
|
||||
if _, err := pbutil.WriteDelimited(&buf, e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func createFlushLog(groupFingerprint uint64, ts, expiresAt time.Time) *flushlogpb.MeshFlushLog {
|
||||
return &flushlogpb.MeshFlushLog{
|
||||
FlushLog: &flushlogpb.FlushLog{
|
||||
GroupFingerprint: groupFingerprint,
|
||||
Timestamp: ts,
|
||||
},
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
}
|
||||
|
||||
// decodeFlushLogState copied from decodeState in prometheus-alertmanager/flushlog/flushlog.go
|
||||
func decodeFlushLogState(r io.Reader) (flushLogState, error) {
|
||||
st := flushLogState{}
|
||||
for {
|
||||
var e flushlogpb.MeshFlushLog
|
||||
_, err := pbutil.ReadDelimited(r, &e)
|
||||
if err == nil {
|
||||
if e.FlushLog == nil || e.FlushLog.GroupFingerprint == 0 || e.FlushLog.Timestamp.IsZero() {
|
||||
return nil, errInvalidState
|
||||
}
|
||||
st[e.FlushLog.GroupFingerprint] = &e
|
||||
continue
|
||||
}
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
type call struct {
|
||||
Method string
|
||||
Args []interface{}
|
||||
|
||||
@@ -47,6 +47,7 @@ import (
|
||||
type stateStore interface {
|
||||
GetSilences(ctx context.Context) (string, error)
|
||||
GetNotificationLog(ctx context.Context) (string, error)
|
||||
GetFlushLog(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
// AutogenFn is a function that adds auto-generated routes to a configuration.
|
||||
@@ -86,6 +87,8 @@ type Alertmanager struct {
|
||||
|
||||
promoteConfig bool
|
||||
externalURL string
|
||||
|
||||
runtimeConfig remoteClient.RuntimeConfig
|
||||
}
|
||||
|
||||
type AlertmanagerConfig struct {
|
||||
@@ -111,6 +114,9 @@ type AlertmanagerConfig struct {
|
||||
|
||||
// Timeout for the HTTP client.
|
||||
Timeout time.Duration
|
||||
|
||||
// RuntimeConfig specifies runtime behavior settings for the remote Alertmanager.
|
||||
RuntimeConfig remoteClient.RuntimeConfig
|
||||
}
|
||||
|
||||
func (cfg *AlertmanagerConfig) Validate() error {
|
||||
@@ -203,6 +209,7 @@ func NewAlertmanager(ctx context.Context, cfg AlertmanagerConfig, store stateSto
|
||||
externalURL: cfg.ExternalURL,
|
||||
promoteConfig: cfg.PromoteConfig,
|
||||
smtp: cfg.SmtpConfig,
|
||||
runtimeConfig: cfg.RuntimeConfig,
|
||||
}
|
||||
|
||||
// Parse the default configuration once and remember its hash so we can compare it later.
|
||||
@@ -331,10 +338,11 @@ func (am *Alertmanager) buildConfiguration(ctx context.Context, raw []byte, crea
|
||||
AlertmanagerConfig: mergeResult.Config,
|
||||
Templates: templates,
|
||||
},
|
||||
CreatedAt: createdAtEpoch,
|
||||
Promoted: am.promoteConfig,
|
||||
ExternalURL: am.externalURL,
|
||||
SmtpConfig: am.smtp,
|
||||
CreatedAt: createdAtEpoch,
|
||||
Promoted: am.promoteConfig,
|
||||
ExternalURL: am.externalURL,
|
||||
SmtpConfig: am.smtp,
|
||||
RuntimeConfig: am.runtimeConfig,
|
||||
}
|
||||
|
||||
cfgHash, err := calculateUserGrafanaConfigHash(payload)
|
||||
@@ -388,6 +396,8 @@ func (am *Alertmanager) GetRemoteState(ctx context.Context) (notifier.ExternalSt
|
||||
rs.Silences = p.Data
|
||||
case "nfl":
|
||||
rs.Nflog = p.Data
|
||||
case "fls":
|
||||
rs.FlushLog = p.Data
|
||||
default:
|
||||
return rs, fmt.Errorf("unknown part key %q", p.Key)
|
||||
}
|
||||
@@ -677,6 +687,12 @@ func (am *Alertmanager) getFullState(ctx context.Context) (string, error) {
|
||||
}
|
||||
parts = append(parts, alertingClusterPB.Part{Key: notifier.NotificationLogFilename, Data: []byte(notificationLog)})
|
||||
|
||||
flushLog, err := am.state.GetFlushLog(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error getting flush log: %w", err)
|
||||
}
|
||||
parts = append(parts, alertingClusterPB.Part{Key: notifier.FlushLogFilename, Data: []byte(flushLog)})
|
||||
|
||||
fs := alertingClusterPB.FullState{
|
||||
Parts: parts,
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ func (u *GrafanaAlertmanagerConfig) MarshalJSON() ([]byte, error) {
|
||||
return definition.MarshalJSONWithSecrets((*cfg)(u))
|
||||
}
|
||||
|
||||
type RuntimeConfig struct {
|
||||
DispatchTimer string `json:"dispatch_timer"`
|
||||
}
|
||||
|
||||
type UserGrafanaConfig struct {
|
||||
GrafanaAlertmanagerConfig GrafanaAlertmanagerConfig `json:"configuration"`
|
||||
Hash string `json:"configuration_hash"`
|
||||
@@ -37,6 +41,7 @@ type UserGrafanaConfig struct {
|
||||
Promoted bool `json:"promoted"`
|
||||
ExternalURL string `json:"external_url"`
|
||||
SmtpConfig SmtpConfig `json:"smtp_config"`
|
||||
RuntimeConfig RuntimeConfig `json:"runtime_config"`
|
||||
}
|
||||
|
||||
func (mc *Mimir) GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafanaConfig, error) {
|
||||
|
||||
@@ -600,6 +600,7 @@ type Cfg struct {
|
||||
IndexRebuildInterval time.Duration
|
||||
IndexCacheTTL time.Duration
|
||||
IndexMinUpdateInterval time.Duration // Don't update index if it was updated less than this interval ago.
|
||||
IndexScoringModel string // Note: Temporary config to switch the index scoring model and will be removed soon.
|
||||
MaxFileIndexAge time.Duration // Max age of file-based indexes. Index older than this will be rebuilt asynchronously.
|
||||
MinFileIndexBuildVersion string // Minimum version of Grafana that built the file-based index. If index was built with older Grafana, it will be rebuilt asynchronously.
|
||||
EnableSharding bool
|
||||
|
||||
@@ -123,6 +123,10 @@ func (cfg *Cfg) setUnifiedStorageConfig() {
|
||||
cfg.IndexRebuildInterval = section.Key("index_rebuild_interval").MustDuration(24 * time.Hour)
|
||||
cfg.IndexCacheTTL = section.Key("index_cache_ttl").MustDuration(10 * time.Minute)
|
||||
cfg.IndexMinUpdateInterval = section.Key("index_min_update_interval").MustDuration(0)
|
||||
cfg.IndexScoringModel = section.Key("index_scoring_model").MustString("")
|
||||
if cfg.IndexScoringModel != "" {
|
||||
cfg.Logger.Info("Index scoring model set", "model", cfg.IndexScoringModel)
|
||||
}
|
||||
cfg.SprinklesApiServer = section.Key("sprinkles_api_server").String()
|
||||
cfg.SprinklesApiServerPageLimit = section.Key("sprinkles_api_server_page_limit").MustInt(10000)
|
||||
cfg.CACertPath = section.Key("ca_cert_path").String()
|
||||
|
||||
@@ -9,11 +9,13 @@ import "resource.proto";
|
||||
// Unlike the ResourceStore, this service can be exposed to clients directly
|
||||
// It should be implemented with efficient indexes and does not need read-after-write semantics
|
||||
service ResourceIndex {
|
||||
// Query for documents
|
||||
rpc Search(ResourceSearchRequest) returns (ResourceSearchResponse);
|
||||
|
||||
// Get the resource stats
|
||||
rpc GetStats(ResourceStatsRequest) returns (ResourceStatsResponse);
|
||||
|
||||
// Rebuild the search index
|
||||
rpc RebuildIndexes(RebuildIndexesRequest) returns (RebuildIndexesResponse);
|
||||
}
|
||||
|
||||
@@ -49,6 +51,20 @@ message ResourceStatsResponse {
|
||||
repeated Stats stats = 2;
|
||||
}
|
||||
|
||||
// This controls what query and analyzers are applied to the specified field
|
||||
// See: https://blevesearch.com/docs/Analyzers/
|
||||
enum QueryFieldType {
|
||||
// Picks a reasonable analyzer given the input. Currently this always uses TEXT
|
||||
// In the future, it may change to depend on the indexed field type
|
||||
DEFAULT = 0;
|
||||
// Use free text analyzer. The query is broken into a normalized set of tokens
|
||||
TEXT = 1;
|
||||
// The query must exactly match the indexed token
|
||||
KEYWORD = 2;
|
||||
// Like a text query, but the position and offsets influence the score
|
||||
PHRASE = 3;
|
||||
}
|
||||
|
||||
// Search within a single resource
|
||||
message ResourceSearchRequest {
|
||||
message Sort {
|
||||
@@ -64,6 +80,18 @@ message ResourceSearchRequest {
|
||||
// date queries
|
||||
}
|
||||
|
||||
// Defines the field in the index to query
|
||||
// Boost is optional, and allows weighting the field higher in the results
|
||||
message QueryField {
|
||||
// The field name in the index to query
|
||||
string name = 1;
|
||||
|
||||
QueryFieldType type = 2;
|
||||
|
||||
// Boost value for this field
|
||||
float boost = 3;
|
||||
}
|
||||
|
||||
// The key must include namespace + group + resource
|
||||
ListOptions options = 1;
|
||||
|
||||
@@ -99,6 +127,9 @@ message ResourceSearchRequest {
|
||||
int64 page = 11;
|
||||
|
||||
int64 permission = 12;
|
||||
|
||||
// Optionally specify which fields are included in the query
|
||||
repeated QueryField query_fields = 13;
|
||||
}
|
||||
|
||||
message ResourceSearchResponse {
|
||||
|
||||
@@ -290,7 +290,6 @@ const SEARCH_FIELD_NAMESPACE = "namespace"
|
||||
const SEARCH_FIELD_NAME = "name"
|
||||
const SEARCH_FIELD_RV = "rv"
|
||||
const SEARCH_FIELD_TITLE = "title"
|
||||
const SEARCH_FIELD_TITLE_NGRAM = "title_ngram"
|
||||
const SEARCH_FIELD_TITLE_PHRASE = "title_phrase" // filtering/sorting on title by full phrase
|
||||
const SEARCH_FIELD_DESCRIPTION = "description"
|
||||
const SEARCH_FIELD_TAGS = "tags"
|
||||
|
||||
@@ -21,6 +21,65 @@ const (
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// This controls what query and analyzers are applied to the specified field
|
||||
// See: https://blevesearch.com/docs/Analyzers/
|
||||
type QueryFieldType int32
|
||||
|
||||
const (
|
||||
// Picks a reasonable analyzer given the input. Currently this always uses TEXT
|
||||
// In the future, it may change to depend on the indexed field type
|
||||
QueryFieldType_DEFAULT QueryFieldType = 0
|
||||
// Use free text analyzer. The query is broken into a normalized set of tokens
|
||||
QueryFieldType_TEXT QueryFieldType = 1
|
||||
// The query must exactly match the indexed token
|
||||
QueryFieldType_KEYWORD QueryFieldType = 2
|
||||
// Like a text query, but the position and offsets influence the score
|
||||
QueryFieldType_PHRASE QueryFieldType = 3
|
||||
)
|
||||
|
||||
// Enum value maps for QueryFieldType.
|
||||
var (
|
||||
QueryFieldType_name = map[int32]string{
|
||||
0: "DEFAULT",
|
||||
1: "TEXT",
|
||||
2: "KEYWORD",
|
||||
3: "PHRASE",
|
||||
}
|
||||
QueryFieldType_value = map[string]int32{
|
||||
"DEFAULT": 0,
|
||||
"TEXT": 1,
|
||||
"KEYWORD": 2,
|
||||
"PHRASE": 3,
|
||||
}
|
||||
)
|
||||
|
||||
func (x QueryFieldType) Enum() *QueryFieldType {
|
||||
p := new(QueryFieldType)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x QueryFieldType) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (QueryFieldType) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_search_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (QueryFieldType) Type() protoreflect.EnumType {
|
||||
return &file_search_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x QueryFieldType) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use QueryFieldType.Descriptor instead.
|
||||
func (QueryFieldType) EnumDescriptor() ([]byte, []int) {
|
||||
return file_search_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
// Get statistics across multiple resources
|
||||
// For these queries, we do not need authorization to see the actual values
|
||||
type ResourceStatsRequest struct {
|
||||
@@ -165,10 +224,12 @@ type ResourceSearchRequest struct {
|
||||
// the return fields (empty will return everything)
|
||||
Fields []string `protobuf:"bytes,8,rep,name=fields,proto3" json:"fields,omitempty"`
|
||||
// explain each result (added to the each row)
|
||||
Explain bool `protobuf:"varint,9,opt,name=explain,proto3" json:"explain,omitempty"`
|
||||
IsDeleted bool `protobuf:"varint,10,opt,name=is_deleted,json=isDeleted,proto3" json:"is_deleted,omitempty"`
|
||||
Page int64 `protobuf:"varint,11,opt,name=page,proto3" json:"page,omitempty"`
|
||||
Permission int64 `protobuf:"varint,12,opt,name=permission,proto3" json:"permission,omitempty"`
|
||||
Explain bool `protobuf:"varint,9,opt,name=explain,proto3" json:"explain,omitempty"`
|
||||
IsDeleted bool `protobuf:"varint,10,opt,name=is_deleted,json=isDeleted,proto3" json:"is_deleted,omitempty"`
|
||||
Page int64 `protobuf:"varint,11,opt,name=page,proto3" json:"page,omitempty"`
|
||||
Permission int64 `protobuf:"varint,12,opt,name=permission,proto3" json:"permission,omitempty"`
|
||||
// Optionally specify which fields are included in the query
|
||||
QueryFields []*ResourceSearchRequest_QueryField `protobuf:"bytes,13,rep,name=query_fields,json=queryFields,proto3" json:"query_fields,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -287,6 +348,13 @@ func (x *ResourceSearchRequest) GetPermission() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ResourceSearchRequest) GetQueryFields() []*ResourceSearchRequest_QueryField {
|
||||
if x != nil {
|
||||
return x.QueryFields
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ResourceSearchResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Error details
|
||||
@@ -670,6 +738,70 @@ func (x *ResourceSearchRequest_Facet) GetLimit() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Defines the field in the index to query
|
||||
// Boost is optional, and allows weighting the field higher in the results
|
||||
type ResourceSearchRequest_QueryField struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// The field name in the index to query
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Type QueryFieldType `protobuf:"varint,2,opt,name=type,proto3,enum=resource.QueryFieldType" json:"type,omitempty"`
|
||||
// Boost value for this field
|
||||
Boost float32 `protobuf:"fixed32,3,opt,name=boost,proto3" json:"boost,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ResourceSearchRequest_QueryField) Reset() {
|
||||
*x = ResourceSearchRequest_QueryField{}
|
||||
mi := &file_search_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ResourceSearchRequest_QueryField) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ResourceSearchRequest_QueryField) ProtoMessage() {}
|
||||
|
||||
func (x *ResourceSearchRequest_QueryField) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_search_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ResourceSearchRequest_QueryField.ProtoReflect.Descriptor instead.
|
||||
func (*ResourceSearchRequest_QueryField) Descriptor() ([]byte, []int) {
|
||||
return file_search_proto_rawDescGZIP(), []int{2, 2}
|
||||
}
|
||||
|
||||
func (x *ResourceSearchRequest_QueryField) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ResourceSearchRequest_QueryField) GetType() QueryFieldType {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return QueryFieldType_DEFAULT
|
||||
}
|
||||
|
||||
func (x *ResourceSearchRequest_QueryField) GetBoost() float32 {
|
||||
if x != nil {
|
||||
return x.Boost
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type ResourceSearchResponse_Facet struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Field string `protobuf:"bytes,1,opt,name=field,proto3" json:"field,omitempty"`
|
||||
@@ -685,7 +817,7 @@ type ResourceSearchResponse_Facet struct {
|
||||
|
||||
func (x *ResourceSearchResponse_Facet) Reset() {
|
||||
*x = ResourceSearchResponse_Facet{}
|
||||
mi := &file_search_proto_msgTypes[10]
|
||||
mi := &file_search_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -697,7 +829,7 @@ func (x *ResourceSearchResponse_Facet) String() string {
|
||||
func (*ResourceSearchResponse_Facet) ProtoMessage() {}
|
||||
|
||||
func (x *ResourceSearchResponse_Facet) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_search_proto_msgTypes[10]
|
||||
mi := &file_search_proto_msgTypes[11]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -751,7 +883,7 @@ type ResourceSearchResponse_TermFacet struct {
|
||||
|
||||
func (x *ResourceSearchResponse_TermFacet) Reset() {
|
||||
*x = ResourceSearchResponse_TermFacet{}
|
||||
mi := &file_search_proto_msgTypes[11]
|
||||
mi := &file_search_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -763,7 +895,7 @@ func (x *ResourceSearchResponse_TermFacet) String() string {
|
||||
func (*ResourceSearchResponse_TermFacet) ProtoMessage() {}
|
||||
|
||||
func (x *ResourceSearchResponse_TermFacet) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_search_proto_msgTypes[11]
|
||||
mi := &file_search_proto_msgTypes[12]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -818,7 +950,7 @@ var file_search_proto_rawDesc = string([]byte{
|
||||
0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63,
|
||||
0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e,
|
||||
0x74, 0x22, 0x8e, 0x05, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65,
|
||||
0x74, 0x22, 0xc3, 0x06, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65,
|
||||
0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x6f,
|
||||
0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72,
|
||||
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69,
|
||||
@@ -846,93 +978,109 @@ var file_search_proto_rawDesc = string([]byte{
|
||||
0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x0b,
|
||||
0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65,
|
||||
0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a,
|
||||
0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f,
|
||||
0x72, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x1a, 0x33, 0x0a, 0x05,
|
||||
0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c,
|
||||
0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69,
|
||||
0x74, 0x1a, 0x5f, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12,
|
||||
0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65,
|
||||
0x79, 0x12, 0x3b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f,
|
||||
0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x4d, 0x0a, 0x0c, 0x71, 0x75,
|
||||
0x65, 0x72, 0x79, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b,
|
||||
0x32, 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f,
|
||||
0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||
0x74, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02,
|
||||
0x38, 0x01, 0x22, 0xea, 0x04, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53,
|
||||
0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a,
|
||||
0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72,
|
||||
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73,
|
||||
0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65,
|
||||
0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
|
||||
0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03,
|
||||
0x6b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x03,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
|
||||
0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x07, 0x72,
|
||||
0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f,
|
||||
0x68, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61,
|
||||
0x6c, 0x48, 0x69, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x63,
|
||||
0x6f, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x71, 0x75, 0x65, 0x72, 0x79,
|
||||
0x43, 0x6f, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x63, 0x6f, 0x72,
|
||||
0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x53, 0x63, 0x6f, 0x72,
|
||||
0x65, 0x12, 0x41, 0x0a, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b,
|
||||
0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f,
|
||||
0x74, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x0b, 0x71, 0x75,
|
||||
0x65, 0x72, 0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, 0x72,
|
||||
0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x1a, 0x33, 0x0a, 0x05, 0x46,
|
||||
0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69,
|
||||
0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74,
|
||||
0x1a, 0x64, 0x0a, 0x0a, 0x51, 0x75, 0x65, 0x72, 0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12,
|
||||
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
|
||||
0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e,
|
||||
0x32, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x51, 0x75, 0x65, 0x72,
|
||||
0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65,
|
||||
0x12, 0x14, 0x0a, 0x05, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x52,
|
||||
0x05, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x1a, 0x5f, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45,
|
||||
0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
||||
0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52,
|
||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61,
|
||||
0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xea, 0x04, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f,
|
||||
0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66,
|
||||
0x61, 0x63, 0x65, 0x74, 0x1a, 0x8f, 0x01, 0x0a, 0x05, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x14,
|
||||
0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66,
|
||||
0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x69,
|
||||
0x73, 0x73, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6d, 0x69, 0x73,
|
||||
0x73, 0x69, 0x6e, 0x67, 0x12, 0x40, 0x0a, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x04, 0x20,
|
||||
0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52,
|
||||
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73,
|
||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52,
|
||||
0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x1a, 0x35, 0x0a, 0x09, 0x54, 0x65, 0x72, 0x6d, 0x46, 0x61,
|
||||
0x63, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x1a, 0x60, 0x0a,
|
||||
0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b,
|
||||
0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3c, 0x0a,
|
||||
0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x72,
|
||||
0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72,
|
||||
0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12,
|
||||
0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72,
|
||||
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
||||
0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46,
|
||||
0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22,
|
||||
0x60, 0x0a, 0x15, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65,
|
||||
0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65,
|
||||
0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d,
|
||||
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x29, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x02,
|
||||
0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
|
||||
0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x04, 0x6b, 0x65, 0x79,
|
||||
0x73, 0x22, 0x83, 0x01, 0x0a, 0x16, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64,
|
||||
0x65, 0x78, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c,
|
||||
0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x03, 0x52, 0x0c, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74,
|
||||
0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72,
|
||||
0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f,
|
||||
0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74,
|
||||
0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x32, 0xfe, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f,
|
||||
0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x4b, 0x0a, 0x06, 0x53, 0x65, 0x61,
|
||||
0x72, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52,
|
||||
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
|
||||
0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65,
|
||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61,
|
||||
0x74, 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65,
|
||||
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||
0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65,
|
||||
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x0e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e,
|
||||
0x64, 0x65, 0x78, 0x65, 0x73, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
||||
0x2e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52,
|
||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
|
||||
0x65, 0x2e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68,
|
||||
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67,
|
||||
0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61,
|
||||
0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75,
|
||||
0x72, 0x63, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75,
|
||||
0x6c, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f,
|
||||
0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62,
|
||||
0x6c, 0x65, 0x52, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74,
|
||||
0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52,
|
||||
0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x48, 0x69, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x71, 0x75,
|
||||
0x65, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09,
|
||||
0x71, 0x75, 0x65, 0x72, 0x79, 0x43, 0x6f, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78,
|
||||
0x5f, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x6d, 0x61,
|
||||
0x78, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x41, 0x0a, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18,
|
||||
0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
||||
0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74,
|
||||
0x72, 0x79, 0x52, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x1a, 0x8f, 0x01, 0x0a, 0x05, 0x46, 0x61,
|
||||
0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74,
|
||||
0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12,
|
||||
0x18, 0x0a, 0x07, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x07, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x12, 0x40, 0x0a, 0x05, 0x74, 0x65, 0x72,
|
||||
0x6d, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
|
||||
0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72,
|
||||
0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x46,
|
||||
0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x1a, 0x35, 0x0a, 0x09, 0x54,
|
||||
0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05,
|
||||
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75,
|
||||
0x6e, 0x74, 0x1a, 0x60, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79,
|
||||
0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
|
||||
0x65, 0x79, 0x12, 0x3c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x26, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73,
|
||||
0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
|
||||
0x3a, 0x02, 0x38, 0x01, 0x22, 0x60, 0x0a, 0x15, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49,
|
||||
0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a,
|
||||
0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x29, 0x0a, 0x04, 0x6b,
|
||||
0x65, 0x79, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f,
|
||||
0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79,
|
||||
0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x22, 0x83, 0x01, 0x0a, 0x16, 0x52, 0x65, 0x62, 0x75, 0x69,
|
||||
0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x43, 0x6f, 0x75, 0x6e,
|
||||
0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64,
|
||||
0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12,
|
||||
0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15,
|
||||
0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52,
|
||||
0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x2a, 0x40, 0x0a, 0x0e,
|
||||
0x51, 0x75, 0x65, 0x72, 0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b,
|
||||
0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x54,
|
||||
0x45, 0x58, 0x54, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x4b, 0x45, 0x59, 0x57, 0x4f, 0x52, 0x44,
|
||||
0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x48, 0x52, 0x41, 0x53, 0x45, 0x10, 0x03, 0x32, 0xfe,
|
||||
0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78,
|
||||
0x12, 0x4b, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73,
|
||||
0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65,
|
||||
0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65,
|
||||
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53,
|
||||
0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a,
|
||||
0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f,
|
||||
0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61,
|
||||
0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f,
|
||||
0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61,
|
||||
0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x0e, 0x52, 0x65,
|
||||
0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x12, 0x1f, 0x2e, 0x72,
|
||||
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49,
|
||||
0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e,
|
||||
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64,
|
||||
0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42,
|
||||
0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72,
|
||||
0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b,
|
||||
0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65,
|
||||
0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x33,
|
||||
})
|
||||
|
||||
var (
|
||||
@@ -947,53 +1095,58 @@ func file_search_proto_rawDescGZIP() []byte {
|
||||
return file_search_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_search_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
|
||||
var file_search_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_search_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
|
||||
var file_search_proto_goTypes = []any{
|
||||
(*ResourceStatsRequest)(nil), // 0: resource.ResourceStatsRequest
|
||||
(*ResourceStatsResponse)(nil), // 1: resource.ResourceStatsResponse
|
||||
(*ResourceSearchRequest)(nil), // 2: resource.ResourceSearchRequest
|
||||
(*ResourceSearchResponse)(nil), // 3: resource.ResourceSearchResponse
|
||||
(*RebuildIndexesRequest)(nil), // 4: resource.RebuildIndexesRequest
|
||||
(*RebuildIndexesResponse)(nil), // 5: resource.RebuildIndexesResponse
|
||||
(*ResourceStatsResponse_Stats)(nil), // 6: resource.ResourceStatsResponse.Stats
|
||||
(*ResourceSearchRequest_Sort)(nil), // 7: resource.ResourceSearchRequest.Sort
|
||||
(*ResourceSearchRequest_Facet)(nil), // 8: resource.ResourceSearchRequest.Facet
|
||||
nil, // 9: resource.ResourceSearchRequest.FacetEntry
|
||||
(*ResourceSearchResponse_Facet)(nil), // 10: resource.ResourceSearchResponse.Facet
|
||||
(*ResourceSearchResponse_TermFacet)(nil), // 11: resource.ResourceSearchResponse.TermFacet
|
||||
nil, // 12: resource.ResourceSearchResponse.FacetEntry
|
||||
(*ErrorResult)(nil), // 13: resource.ErrorResult
|
||||
(*ListOptions)(nil), // 14: resource.ListOptions
|
||||
(*ResourceKey)(nil), // 15: resource.ResourceKey
|
||||
(*ResourceTable)(nil), // 16: resource.ResourceTable
|
||||
(QueryFieldType)(0), // 0: resource.QueryFieldType
|
||||
(*ResourceStatsRequest)(nil), // 1: resource.ResourceStatsRequest
|
||||
(*ResourceStatsResponse)(nil), // 2: resource.ResourceStatsResponse
|
||||
(*ResourceSearchRequest)(nil), // 3: resource.ResourceSearchRequest
|
||||
(*ResourceSearchResponse)(nil), // 4: resource.ResourceSearchResponse
|
||||
(*RebuildIndexesRequest)(nil), // 5: resource.RebuildIndexesRequest
|
||||
(*RebuildIndexesResponse)(nil), // 6: resource.RebuildIndexesResponse
|
||||
(*ResourceStatsResponse_Stats)(nil), // 7: resource.ResourceStatsResponse.Stats
|
||||
(*ResourceSearchRequest_Sort)(nil), // 8: resource.ResourceSearchRequest.Sort
|
||||
(*ResourceSearchRequest_Facet)(nil), // 9: resource.ResourceSearchRequest.Facet
|
||||
(*ResourceSearchRequest_QueryField)(nil), // 10: resource.ResourceSearchRequest.QueryField
|
||||
nil, // 11: resource.ResourceSearchRequest.FacetEntry
|
||||
(*ResourceSearchResponse_Facet)(nil), // 12: resource.ResourceSearchResponse.Facet
|
||||
(*ResourceSearchResponse_TermFacet)(nil), // 13: resource.ResourceSearchResponse.TermFacet
|
||||
nil, // 14: resource.ResourceSearchResponse.FacetEntry
|
||||
(*ErrorResult)(nil), // 15: resource.ErrorResult
|
||||
(*ListOptions)(nil), // 16: resource.ListOptions
|
||||
(*ResourceKey)(nil), // 17: resource.ResourceKey
|
||||
(*ResourceTable)(nil), // 18: resource.ResourceTable
|
||||
}
|
||||
var file_search_proto_depIdxs = []int32{
|
||||
13, // 0: resource.ResourceStatsResponse.error:type_name -> resource.ErrorResult
|
||||
6, // 1: resource.ResourceStatsResponse.stats:type_name -> resource.ResourceStatsResponse.Stats
|
||||
14, // 2: resource.ResourceSearchRequest.options:type_name -> resource.ListOptions
|
||||
15, // 3: resource.ResourceSearchRequest.federated:type_name -> resource.ResourceKey
|
||||
7, // 4: resource.ResourceSearchRequest.sortBy:type_name -> resource.ResourceSearchRequest.Sort
|
||||
9, // 5: resource.ResourceSearchRequest.facet:type_name -> resource.ResourceSearchRequest.FacetEntry
|
||||
13, // 6: resource.ResourceSearchResponse.error:type_name -> resource.ErrorResult
|
||||
15, // 7: resource.ResourceSearchResponse.key:type_name -> resource.ResourceKey
|
||||
16, // 8: resource.ResourceSearchResponse.results:type_name -> resource.ResourceTable
|
||||
12, // 9: resource.ResourceSearchResponse.facet:type_name -> resource.ResourceSearchResponse.FacetEntry
|
||||
15, // 10: resource.RebuildIndexesRequest.keys:type_name -> resource.ResourceKey
|
||||
13, // 11: resource.RebuildIndexesResponse.error:type_name -> resource.ErrorResult
|
||||
8, // 12: resource.ResourceSearchRequest.FacetEntry.value:type_name -> resource.ResourceSearchRequest.Facet
|
||||
11, // 13: resource.ResourceSearchResponse.Facet.terms:type_name -> resource.ResourceSearchResponse.TermFacet
|
||||
10, // 14: resource.ResourceSearchResponse.FacetEntry.value:type_name -> resource.ResourceSearchResponse.Facet
|
||||
2, // 15: resource.ResourceIndex.Search:input_type -> resource.ResourceSearchRequest
|
||||
0, // 16: resource.ResourceIndex.GetStats:input_type -> resource.ResourceStatsRequest
|
||||
4, // 17: resource.ResourceIndex.RebuildIndexes:input_type -> resource.RebuildIndexesRequest
|
||||
3, // 18: resource.ResourceIndex.Search:output_type -> resource.ResourceSearchResponse
|
||||
1, // 19: resource.ResourceIndex.GetStats:output_type -> resource.ResourceStatsResponse
|
||||
5, // 20: resource.ResourceIndex.RebuildIndexes:output_type -> resource.RebuildIndexesResponse
|
||||
18, // [18:21] is the sub-list for method output_type
|
||||
15, // [15:18] is the sub-list for method input_type
|
||||
15, // [15:15] is the sub-list for extension type_name
|
||||
15, // [15:15] is the sub-list for extension extendee
|
||||
0, // [0:15] is the sub-list for field type_name
|
||||
15, // 0: resource.ResourceStatsResponse.error:type_name -> resource.ErrorResult
|
||||
7, // 1: resource.ResourceStatsResponse.stats:type_name -> resource.ResourceStatsResponse.Stats
|
||||
16, // 2: resource.ResourceSearchRequest.options:type_name -> resource.ListOptions
|
||||
17, // 3: resource.ResourceSearchRequest.federated:type_name -> resource.ResourceKey
|
||||
8, // 4: resource.ResourceSearchRequest.sortBy:type_name -> resource.ResourceSearchRequest.Sort
|
||||
11, // 5: resource.ResourceSearchRequest.facet:type_name -> resource.ResourceSearchRequest.FacetEntry
|
||||
10, // 6: resource.ResourceSearchRequest.query_fields:type_name -> resource.ResourceSearchRequest.QueryField
|
||||
15, // 7: resource.ResourceSearchResponse.error:type_name -> resource.ErrorResult
|
||||
17, // 8: resource.ResourceSearchResponse.key:type_name -> resource.ResourceKey
|
||||
18, // 9: resource.ResourceSearchResponse.results:type_name -> resource.ResourceTable
|
||||
14, // 10: resource.ResourceSearchResponse.facet:type_name -> resource.ResourceSearchResponse.FacetEntry
|
||||
17, // 11: resource.RebuildIndexesRequest.keys:type_name -> resource.ResourceKey
|
||||
15, // 12: resource.RebuildIndexesResponse.error:type_name -> resource.ErrorResult
|
||||
0, // 13: resource.ResourceSearchRequest.QueryField.type:type_name -> resource.QueryFieldType
|
||||
9, // 14: resource.ResourceSearchRequest.FacetEntry.value:type_name -> resource.ResourceSearchRequest.Facet
|
||||
13, // 15: resource.ResourceSearchResponse.Facet.terms:type_name -> resource.ResourceSearchResponse.TermFacet
|
||||
12, // 16: resource.ResourceSearchResponse.FacetEntry.value:type_name -> resource.ResourceSearchResponse.Facet
|
||||
3, // 17: resource.ResourceIndex.Search:input_type -> resource.ResourceSearchRequest
|
||||
1, // 18: resource.ResourceIndex.GetStats:input_type -> resource.ResourceStatsRequest
|
||||
5, // 19: resource.ResourceIndex.RebuildIndexes:input_type -> resource.RebuildIndexesRequest
|
||||
4, // 20: resource.ResourceIndex.Search:output_type -> resource.ResourceSearchResponse
|
||||
2, // 21: resource.ResourceIndex.GetStats:output_type -> resource.ResourceStatsResponse
|
||||
6, // 22: resource.ResourceIndex.RebuildIndexes:output_type -> resource.RebuildIndexesResponse
|
||||
20, // [20:23] is the sub-list for method output_type
|
||||
17, // [17:20] is the sub-list for method input_type
|
||||
17, // [17:17] is the sub-list for extension type_name
|
||||
17, // [17:17] is the sub-list for extension extendee
|
||||
0, // [0:17] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_search_proto_init() }
|
||||
@@ -1007,13 +1160,14 @@ func file_search_proto_init() {
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_search_proto_rawDesc), len(file_search_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 13,
|
||||
NumEnums: 1,
|
||||
NumMessages: 14,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_search_proto_goTypes,
|
||||
DependencyIndexes: file_search_proto_depIdxs,
|
||||
EnumInfos: file_search_proto_enumTypes,
|
||||
MessageInfos: file_search_proto_msgTypes,
|
||||
}.Build()
|
||||
File_search_proto = out.File
|
||||
|
||||
@@ -31,9 +31,11 @@ const (
|
||||
// Unlike the ResourceStore, this service can be exposed to clients directly
|
||||
// It should be implemented with efficient indexes and does not need read-after-write semantics
|
||||
type ResourceIndexClient interface {
|
||||
// Query for documents
|
||||
Search(ctx context.Context, in *ResourceSearchRequest, opts ...grpc.CallOption) (*ResourceSearchResponse, error)
|
||||
// Get the resource stats
|
||||
GetStats(ctx context.Context, in *ResourceStatsRequest, opts ...grpc.CallOption) (*ResourceStatsResponse, error)
|
||||
// Rebuild the search index
|
||||
RebuildIndexes(ctx context.Context, in *RebuildIndexesRequest, opts ...grpc.CallOption) (*RebuildIndexesResponse, error)
|
||||
}
|
||||
|
||||
@@ -82,9 +84,11 @@ func (c *resourceIndexClient) RebuildIndexes(ctx context.Context, in *RebuildInd
|
||||
// Unlike the ResourceStore, this service can be exposed to clients directly
|
||||
// It should be implemented with efficient indexes and does not need read-after-write semantics
|
||||
type ResourceIndexServer interface {
|
||||
// Query for documents
|
||||
Search(context.Context, *ResourceSearchRequest) (*ResourceSearchResponse, error)
|
||||
// Get the resource stats
|
||||
GetStats(context.Context, *ResourceStatsRequest) (*ResourceStatsResponse, error)
|
||||
// Rebuild the search index
|
||||
RebuildIndexes(context.Context, *RebuildIndexesRequest) (*RebuildIndexesResponse, error)
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,11 @@ type BleveOptions struct {
|
||||
// Indexes that are not owned by current instance are eligible for cleanup.
|
||||
// If nil, all indexes are owned by the current instance.
|
||||
OwnsIndex func(key resource.NamespacedResource) (bool, error)
|
||||
|
||||
// ScoringModel defines the scoring model used for the bleve indexes
|
||||
// Default: index.TFIDFScoring
|
||||
// Supported values: index.TFIDFScoring and index.BM25Scoring
|
||||
ScoringModel string
|
||||
}
|
||||
|
||||
type bleveBackend struct {
|
||||
@@ -368,7 +373,7 @@ func (b *bleveBackend) BuildIndex(
|
||||
attribute.String("reason", indexBuildReason),
|
||||
)
|
||||
|
||||
mapper, err := GetBleveMappings(fields)
|
||||
mapper, err := GetBleveMappings(b.opts.ScoringModel, fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1177,6 +1182,7 @@ func (b *bleveIndex) getIndex(
|
||||
return b.index, nil
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resourcepb.ResourceSearchRequest, access authlib.AccessClient) (*bleve.SearchRequest, *resourcepb.ErrorResult) {
|
||||
ctx, span := tracer.Start(ctx, "search.bleveIndex.toBleveSearchRequest")
|
||||
defer span.End()
|
||||
@@ -1235,42 +1241,62 @@ func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resourcepb.R
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.Query) > 1 && strings.Contains(req.Query, "*") {
|
||||
// wildcard query is expensive - should be used with caution
|
||||
wildcard := bleve.NewWildcardQuery(req.Query)
|
||||
queries = append(queries, wildcard)
|
||||
}
|
||||
if len(req.Query) > 1 {
|
||||
if strings.Contains(req.Query, "*") {
|
||||
// wildcard query is expensive - should be used with caution
|
||||
wildcard := bleve.NewWildcardQuery(req.Query)
|
||||
queries = append(queries, wildcard)
|
||||
} else {
|
||||
// When using a
|
||||
searchrequest.Fields = append(searchrequest.Fields, resource.SEARCH_FIELD_SCORE)
|
||||
disjoin := bleve.NewDisjunctionQuery()
|
||||
queries = append(queries, disjoin)
|
||||
|
||||
if req.Query != "" && !strings.Contains(req.Query, "*") {
|
||||
// Add a text query
|
||||
searchrequest.Fields = append(searchrequest.Fields, resource.SEARCH_FIELD_SCORE)
|
||||
queryFields := req.QueryFields
|
||||
if len(queryFields) == 0 {
|
||||
queryFields = []*resourcepb.ResourceSearchRequest_QueryField{
|
||||
{
|
||||
Name: resource.SEARCH_FIELD_TITLE,
|
||||
Type: resourcepb.QueryFieldType_KEYWORD,
|
||||
Boost: 10, // exact match -- includes ngrams! If they lived on their own field, we could score them differently
|
||||
}, {
|
||||
Name: resource.SEARCH_FIELD_TITLE,
|
||||
Type: resourcepb.QueryFieldType_TEXT,
|
||||
Boost: 2, // standard analyzer (with ngrams!)
|
||||
}, {
|
||||
Name: resource.SEARCH_FIELD_TITLE_PHRASE,
|
||||
Type: resourcepb.QueryFieldType_TEXT,
|
||||
Boost: 5, // standard analyzer
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// There are multiple ways to match the query string to documents. The following queries are ordered by priority:
|
||||
for _, field := range queryFields {
|
||||
switch field.Type {
|
||||
case resourcepb.QueryFieldType_TEXT, resourcepb.QueryFieldType_DEFAULT:
|
||||
q := bleve.NewMatchQuery(removeSmallTerms(req.Query)) // removeSmallTerms should be part of the analyzer
|
||||
q.SetBoost(float64(field.Boost))
|
||||
q.SetField(field.Name)
|
||||
q.Analyzer = standard.Name // analyze the text
|
||||
q.Operator = query.MatchQueryOperatorAnd // all terms must match
|
||||
disjoin.AddQuery(q)
|
||||
|
||||
// Query 1: Match the exact query string
|
||||
queryExact := bleve.NewMatchQuery(req.Query)
|
||||
queryExact.SetBoost(10.0)
|
||||
queryExact.SetField(resource.SEARCH_FIELD_TITLE)
|
||||
queryExact.Analyzer = keyword.Name // don't analyze the query input - treat it as a single token
|
||||
queryExact.Operator = query.MatchQueryOperatorAnd // This doesn't make a difference for keyword analyzer, we add it just to be explicit.
|
||||
searchQuery := bleve.NewDisjunctionQuery(queryExact)
|
||||
case resourcepb.QueryFieldType_KEYWORD:
|
||||
q := bleve.NewMatchQuery(req.Query)
|
||||
q.SetBoost(float64(field.Boost))
|
||||
q.SetField(field.Name)
|
||||
q.Analyzer = keyword.Name // don't analyze the query input - treat it as a single token
|
||||
disjoin.AddQuery(q)
|
||||
|
||||
// Query 2: Phrase query with standard analyzer
|
||||
queryPhrase := bleve.NewMatchPhraseQuery(req.Query)
|
||||
queryPhrase.SetBoost(5.0)
|
||||
queryPhrase.SetField(resource.SEARCH_FIELD_TITLE)
|
||||
queryPhrase.Analyzer = standard.Name
|
||||
searchQuery.AddQuery(queryPhrase)
|
||||
|
||||
// Query 3: Match query with standard analyzer
|
||||
queryAnalyzed := bleve.NewMatchQuery(removeSmallTerms(req.Query))
|
||||
queryAnalyzed.SetField(resource.SEARCH_FIELD_TITLE)
|
||||
queryAnalyzed.SetBoost(2.0)
|
||||
queryAnalyzed.Analyzer = standard.Name
|
||||
queryAnalyzed.Operator = query.MatchQueryOperatorAnd // Make sure all terms from the query are matched
|
||||
searchQuery.AddQuery(queryAnalyzed)
|
||||
|
||||
queries = append(queries, searchQuery)
|
||||
case resourcepb.QueryFieldType_PHRASE:
|
||||
q := bleve.NewMatchPhraseQuery(req.Query)
|
||||
q.SetBoost(float64(field.Boost))
|
||||
q.SetField(field.Name)
|
||||
q.Analyzer = standard.Name
|
||||
disjoin.AddQuery(q)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch len(queries) {
|
||||
@@ -1872,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(d *search.DocumentMatch) bool {
|
||||
filteringSearcher := bleveSearch.NewFilteringSearcher(ctx, searcher, func(_ *search.SearchContext, 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
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
index "github.com/blevesearch/bleve_index_api"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
@@ -19,6 +20,7 @@ func TestBleveSearchBackend(t *testing.T) {
|
||||
backend, err := NewBleveBackend(BleveOptions{
|
||||
Root: tempDir,
|
||||
FileThreshold: 5,
|
||||
ScoringModel: index.BM25Scoring,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, backend)
|
||||
@@ -52,3 +54,32 @@ func TestSearchBackendBenchmark(t *testing.T) {
|
||||
|
||||
unitest.BenchmarkSearchBackend(t, backend, opts)
|
||||
}
|
||||
|
||||
func BenchmarkScoringModels(b *testing.B) {
|
||||
models := []string{index.TFIDFScoring, index.BM25Scoring}
|
||||
|
||||
for _, model := range models {
|
||||
b.Run(model, func(b *testing.B) {
|
||||
tempDir := b.TempDir()
|
||||
|
||||
backend, err := NewBleveBackend(BleveOptions{
|
||||
Root: tempDir,
|
||||
ScoringModel: model,
|
||||
}, nil)
|
||||
require.NoError(b, err)
|
||||
require.NotNil(b, backend)
|
||||
|
||||
b.Cleanup(backend.Stop)
|
||||
|
||||
opts := &unitest.BenchmarkOptions{
|
||||
NumResources: 1000,
|
||||
Concurrency: 4,
|
||||
NumNamespaces: 10,
|
||||
NumGroups: 10,
|
||||
NumResourceTypes: 10,
|
||||
}
|
||||
|
||||
unitest.BenchmarkSearchBackend(b, backend, opts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,15 @@ import (
|
||||
"github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
|
||||
"github.com/blevesearch/bleve/v2/analysis/analyzer/standard"
|
||||
"github.com/blevesearch/bleve/v2/mapping"
|
||||
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
)
|
||||
|
||||
func GetBleveMappings(fields resource.SearchableDocumentFields) (mapping.IndexMapping, error) {
|
||||
func GetBleveMappings(scoringModel string, fields resource.SearchableDocumentFields) (mapping.IndexMapping, error) {
|
||||
mapper := bleve.NewIndexMapping()
|
||||
if scoringModel != "" {
|
||||
mapper.ScoringModel = scoringModel
|
||||
}
|
||||
|
||||
err := RegisterCustomAnalyzers(mapper)
|
||||
if err != nil {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func TestDocumentMapping(t *testing.T) {
|
||||
mappings, err := search.GetBleveMappings(nil)
|
||||
mappings, err := search.GetBleveMappings("", nil)
|
||||
require.NoError(t, err)
|
||||
data := resource.IndexableDocument{
|
||||
Title: "title",
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
index "github.com/blevesearch/bleve_index_api"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
@@ -258,6 +259,7 @@ func newTestDashboardsIndex(t testing.TB, threshold int64, size int64, writer re
|
||||
backend, err := search.NewBleveBackend(search.BleveOptions{
|
||||
Root: t.TempDir(),
|
||||
FileThreshold: threshold, // use in-memory for tests
|
||||
ScoringModel: index.BM25Scoring,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
index "github.com/blevesearch/bleve_index_api"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -50,6 +51,7 @@ func TestBleveBackend(t *testing.T) {
|
||||
backend, err := NewBleveBackend(BleveOptions{
|
||||
Root: tmpdir,
|
||||
FileThreshold: 5, // with more than 5 items we create a file on disk
|
||||
ScoringModel: index.BM25Scoring,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(backend.Stop)
|
||||
@@ -773,6 +775,7 @@ func setupBleveBackend(t *testing.T, options ...setupOption) (*bleveBackend, pro
|
||||
IndexCacheTTL: defaultIndexCacheTTL,
|
||||
Logger: log.NewNopLogger(),
|
||||
BuildVersion: buildVersion,
|
||||
ScoringModel: index.BM25Scoring,
|
||||
}
|
||||
for _, opt := range options {
|
||||
opt(&opts)
|
||||
|
||||
@@ -46,6 +46,7 @@ func NewSearchOptions(
|
||||
BuildVersion: cfg.BuildVersion,
|
||||
OwnsIndex: ownsIndexFn,
|
||||
IndexMinUpdateInterval: cfg.IndexMinUpdateInterval,
|
||||
ScoringModel: cfg.IndexScoringModel,
|
||||
}, indexMetrics)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
index "github.com/blevesearch/bleve_index_api"
|
||||
"github.com/go-jose/go-jose/v4/jwt"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -129,21 +130,28 @@ func TestIntegrationSearchAndStorage(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a new bleve backend
|
||||
search, err := search.NewBleveBackend(search.BleveOptions{
|
||||
FileThreshold: 0,
|
||||
Root: t.TempDir(),
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, search)
|
||||
t.Cleanup(search.Stop)
|
||||
scoringModels := []string{index.TFIDFScoring, index.BM25Scoring}
|
||||
|
||||
// Create a new resource backend
|
||||
storage, _ := newTestBackend(t, false, 0)
|
||||
require.NotNil(t, storage)
|
||||
for _, model := range scoringModels {
|
||||
t.Run(model, func(t *testing.T) {
|
||||
// Create a new bleve backend
|
||||
search, err := search.NewBleveBackend(search.BleveOptions{
|
||||
FileThreshold: 0,
|
||||
Root: t.TempDir(),
|
||||
ScoringModel: model,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, search)
|
||||
t.Cleanup(search.Stop)
|
||||
|
||||
// Run the shared storage and search tests
|
||||
unitest.RunTestSearchAndStorage(t, ctx, storage, search)
|
||||
// Create a new resource backend
|
||||
storage, _ := newTestBackend(t, false, 0)
|
||||
require.NotNil(t, storage)
|
||||
|
||||
// Run the shared storage and search tests
|
||||
unitest.RunTestSearchAndStorage(t, ctx, storage, search)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientServer(t *testing.T) {
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
"path": "public/plugins/grafana-azure-monitor-datasource/img/azure_monitor_cpu.png"
|
||||
}
|
||||
],
|
||||
"version": "12.3.0-pre",
|
||||
"version": "12.4.0-pre",
|
||||
"updated": "",
|
||||
"keywords": [
|
||||
"azure",
|
||||
@@ -589,7 +589,7 @@
|
||||
"hasUpdate": false,
|
||||
"defaultNavUrl": "/plugins/datagrid/",
|
||||
"category": "",
|
||||
"state": "beta",
|
||||
"state": "deprecated",
|
||||
"signature": "internal",
|
||||
"signatureType": "",
|
||||
"signatureOrg": "",
|
||||
@@ -880,7 +880,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.3.0-pre",
|
||||
"version": "12.4.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -934,7 +934,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.3.0-pre",
|
||||
"version": "12.4.0-pre",
|
||||
"updated": "",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -1000,7 +1000,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.3.0-pre",
|
||||
"version": "12.4.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -1217,7 +1217,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.3.0-pre",
|
||||
"version": "12.4.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -1325,7 +1325,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.3.0-pre",
|
||||
"version": "12.4.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -1375,7 +1375,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.3.0-pre",
|
||||
"version": "12.4.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -1425,7 +1425,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.3.0-pre",
|
||||
"version": "12.4.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -1575,7 +1575,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"version": "12.4.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -1629,7 +1629,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.3.0-pre",
|
||||
"version": "12.4.0-pre",
|
||||
"updated": "",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -1734,7 +1734,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.3.0-pre",
|
||||
"version": "12.4.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -2042,7 +2042,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.3.0-pre",
|
||||
"version": "12.4.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -2092,7 +2092,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.3.0-pre",
|
||||
"version": "12.4.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -2445,7 +2445,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.3.0-pre",
|
||||
"version": "12.4.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
|
||||
@@ -97,7 +97,7 @@ func TestIntegrationSearchDevDashboards(t *testing.T) {
|
||||
require.Equal(t, 16, fileCount, "file count from %s", devenv)
|
||||
|
||||
// Helper to call search
|
||||
callSearch := func(user apis.User, params string) dashboardV0.SearchResults {
|
||||
callSearch := func(user apis.User, params map[string]string) dashboardV0.SearchResults {
|
||||
require.NotNil(t, user)
|
||||
ns := user.Identity.GetNamespace()
|
||||
cfg := dynamic.ConfigFor(user.NewRestConfig())
|
||||
@@ -107,17 +107,12 @@ func TestIntegrationSearchDevDashboards(t *testing.T) {
|
||||
|
||||
var statusCode int
|
||||
req := restClient.Get().AbsPath("apis", "dashboard.grafana.app", "v0alpha1", "namespaces", ns, "search").
|
||||
//Param("explain", "true") // helpful to understand which field made things match
|
||||
Param("limit", "1000").
|
||||
Param("type", "dashboard") // Only search dashboards
|
||||
|
||||
for kv := range strings.SplitSeq(params, "&") {
|
||||
if kv == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(kv, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
req = req.Param(parts[0], parts[1])
|
||||
}
|
||||
for k, v := range params {
|
||||
req = req.Param(k, v)
|
||||
}
|
||||
res := req.Do(ctx).StatusCode(&statusCode)
|
||||
require.NoError(t, res.Error())
|
||||
@@ -140,22 +135,47 @@ func TestIntegrationSearchDevDashboards(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
user apis.User
|
||||
params string
|
||||
params map[string]string
|
||||
}{
|
||||
{
|
||||
name: "all",
|
||||
user: helper.Org1.Admin,
|
||||
params: "", // only dashboards
|
||||
name: "all",
|
||||
user: helper.Org1.Admin,
|
||||
},
|
||||
{
|
||||
name: "simple-query",
|
||||
user: helper.Org1.Admin,
|
||||
params: "query=stacking",
|
||||
name: "query-single-word",
|
||||
user: helper.Org1.Admin,
|
||||
params: map[string]string{
|
||||
"query": "stacking",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with-text-panel",
|
||||
user: helper.Org1.Admin,
|
||||
params: "field=panel_types&panelType=text",
|
||||
name: "query-multiple-words",
|
||||
user: helper.Org1.Admin,
|
||||
params: map[string]string{
|
||||
"query": "graph softMin", // must match ALL terms
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with-text-panel",
|
||||
user: helper.Org1.Admin,
|
||||
params: map[string]string{
|
||||
"field": "panel_types", // return panel types
|
||||
"panelType": "text",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "title-ngram-prefix",
|
||||
user: helper.Org1.Admin,
|
||||
params: map[string]string{
|
||||
"query": "zer", // should match "Zero Decimals Y Ticks"
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "title-ngram-middle-word",
|
||||
user: helper.Org1.Admin,
|
||||
params: map[string]string{
|
||||
"query": "decim", // should match "Zero Decimals Y Ticks"
|
||||
},
|
||||
},
|
||||
}
|
||||
for i, tc := range testCases {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
],
|
||||
"score": 0.658
|
||||
"score": 0.284
|
||||
},
|
||||
{
|
||||
"resource": "dashboards",
|
||||
@@ -21,8 +21,8 @@
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
],
|
||||
"score": 0.625
|
||||
"score": 0.269
|
||||
}
|
||||
],
|
||||
"maxScore": 0.658
|
||||
"maxScore": 0.284
|
||||
}
|
||||
17
pkg/tests/apis/dashboard/testdata/searchV0/t02-query-multiple-words.json
vendored
Normal file
17
pkg/tests/apis/dashboard/testdata/searchV0/t02-query-multiple-words.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"totalHits": 1,
|
||||
"hits": [
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-soft-limits",
|
||||
"title": "Panel Tests - Graph NG - softMin/softMax",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
],
|
||||
"score": 0.024
|
||||
}
|
||||
],
|
||||
"maxScore": 0.024
|
||||
}
|
||||
17
pkg/tests/apis/dashboard/testdata/searchV0/t04-title-ngram-prefix.json
vendored
Normal file
17
pkg/tests/apis/dashboard/testdata/searchV0/t04-title-ngram-prefix.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"totalHits": 1,
|
||||
"hits": [
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-y-ticks-zero-decimals",
|
||||
"title": "Zero Decimals Y Ticks",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
],
|
||||
"score": 0.35
|
||||
}
|
||||
],
|
||||
"maxScore": 0.35
|
||||
}
|
||||
17
pkg/tests/apis/dashboard/testdata/searchV0/t05-title-ngram-middle-word.json
vendored
Normal file
17
pkg/tests/apis/dashboard/testdata/searchV0/t05-title-ngram-middle-word.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"totalHits": 1,
|
||||
"hits": [
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-y-ticks-zero-decimals",
|
||||
"title": "Zero Decimals Y Ticks",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
],
|
||||
"score": 0.35
|
||||
}
|
||||
],
|
||||
"maxScore": 0.35
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DataQuery } from '@grafana/data';
|
||||
import { createMonitoringLogger, MonitoringLogger } from '@grafana/runtime';
|
||||
import store from 'app/core/store';
|
||||
import { RichHistoryQuery } from 'app/types/explore';
|
||||
|
||||
@@ -26,8 +27,15 @@ jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getBackendSrv: () => backendSrv,
|
||||
getDataSourceSrv: () => dsMock,
|
||||
createMonitoringLogger: jest.fn().mockReturnValue({ logWarning: jest.fn() }),
|
||||
}));
|
||||
|
||||
// logger is created at import so we cannot initialize inside the test
|
||||
const loggerIndex = (createMonitoringLogger as jest.Mock).mock.calls.findIndex(
|
||||
(args) => args[0] === 'features.query-history.local-storage'
|
||||
);
|
||||
const loggerMock: MonitoringLogger = (createMonitoringLogger as jest.Mock).mock.results[loggerIndex]?.value;
|
||||
|
||||
interface MockQuery extends DataQuery {
|
||||
query: string;
|
||||
}
|
||||
@@ -75,6 +83,8 @@ describe('RichHistoryLocalStorage', () => {
|
||||
jest.setSystemTime(now);
|
||||
storage = new RichHistoryLocalStorage();
|
||||
await storage.deleteAll();
|
||||
|
||||
(loggerMock.logWarning as jest.Mock).mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -223,6 +233,90 @@ describe('RichHistoryLocalStorage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('quota errors and retries', () => {
|
||||
it('should rotate and retry saving when QuotaExceededError occurs once', async () => {
|
||||
const initial = [
|
||||
{ ts: Date.now(), starred: true, comment: 'starred1', queries: [], datasourceName: 'name-of-dev-test' },
|
||||
{ ts: Date.now(), starred: false, comment: 'notStarred1', queries: [], datasourceName: 'name-of-dev-test' },
|
||||
{ ts: Date.now(), starred: true, comment: 'starred2', queries: [], datasourceName: 'name-of-dev-test' },
|
||||
];
|
||||
store.setObject(key, initial);
|
||||
|
||||
// Spy on setObject to throw once with QuotaExceededError, then call through
|
||||
const originalSetObject = store.setObject.bind(store);
|
||||
jest
|
||||
.spyOn(store, 'setObject')
|
||||
// first attempt throws and errors
|
||||
.mockImplementationOnce(() => {
|
||||
const err = new Error('quota hit');
|
||||
err.name = 'QuotaExceededError';
|
||||
throw err;
|
||||
})
|
||||
// second attempt calls through
|
||||
.mockImplementation((k: string, value: unknown) => {
|
||||
return originalSetObject(k, value);
|
||||
});
|
||||
|
||||
const result = await storage.addToRichHistory({
|
||||
starred: false,
|
||||
datasourceUid: 'dev-test',
|
||||
datasourceName: 'name-of-dev-test',
|
||||
comment: 'new',
|
||||
queries: [{ refId: 'A' }],
|
||||
});
|
||||
expect(result.richHistoryQuery).toBeDefined();
|
||||
|
||||
// After one failure, rotation removes one unstarred entry
|
||||
const saved = store.getObject<RichHistoryQuery[]>(key)!;
|
||||
expect(saved).toHaveLength(3);
|
||||
expect(saved).toMatchObject([
|
||||
expect.objectContaining({ comment: 'new' }),
|
||||
expect.objectContaining({ comment: 'starred1' }),
|
||||
expect.objectContaining({ comment: 'starred2' }),
|
||||
]);
|
||||
|
||||
// Ensure logger was called for the failure, with expected flags
|
||||
expect(loggerMock.logWarning).toHaveBeenCalled();
|
||||
const [message, payload] = (loggerMock.logWarning as jest.Mock).mock.calls[0];
|
||||
expect(message).toContain('Failed to save rich history to local storage');
|
||||
expect(payload.saveRetriesLeft).toBe('3');
|
||||
expect(payload.quotaExceededError).toBe('true');
|
||||
});
|
||||
|
||||
it('should throw StorageFull when QuotaExceededError persists for all retries and track attempts', async () => {
|
||||
store.setObject(key, [
|
||||
{ ts: Date.now(), starred: false, comment: 'notStarred1', queries: [], datasourceName: 'name-of-dev-test' },
|
||||
]);
|
||||
|
||||
const setSpy = jest.spyOn(store, 'setObject').mockImplementation(() => {
|
||||
const err = new Error('quota still hit');
|
||||
err.name = 'QuotaExceededError';
|
||||
throw err;
|
||||
});
|
||||
|
||||
await expect(
|
||||
storage.addToRichHistory({
|
||||
starred: false,
|
||||
datasourceUid: 'dev-test',
|
||||
datasourceName: 'name-of-dev-test',
|
||||
comment: 'new',
|
||||
queries: [{ refId: 'B' }],
|
||||
})
|
||||
).rejects.toMatchObject({ name: 'StorageFull' });
|
||||
|
||||
// 4 failed tracking attempts (1 save + 3 retries) should be logged (for each failed try)
|
||||
expect(loggerMock.logWarning).toHaveBeenCalledTimes(4);
|
||||
const calls = (loggerMock.logWarning as jest.Mock).mock.calls;
|
||||
expect(calls[0][0]).toContain('Failed to save rich history to local storage');
|
||||
expect(calls[0][1].saveRetriesLeft).toBe('3');
|
||||
expect(calls[1][1].saveRetriesLeft).toBe('2');
|
||||
expect(calls[2][1].saveRetriesLeft).toBe('1');
|
||||
expect(calls[3][1].saveRetriesLeft).toBe('0');
|
||||
|
||||
setSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('migration', () => {
|
||||
afterEach(() => {
|
||||
storage.deleteAll();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { find, isEqual, omit } from 'lodash';
|
||||
|
||||
import { DataQuery, SelectableValue } from '@grafana/data';
|
||||
import { RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistoryTypes';
|
||||
import { createMonitoringLogger } from '@grafana/runtime';
|
||||
import { RichHistorySearchFilters, RichHistorySettings, SortOrder } from 'app/core/utils/richHistoryTypes';
|
||||
import { RichHistoryQuery } from 'app/types/explore';
|
||||
|
||||
import store from '../store';
|
||||
@@ -26,10 +27,18 @@ export type RichHistoryLocalStorageDTO = {
|
||||
queries: DataQuery[];
|
||||
};
|
||||
|
||||
const logger = createMonitoringLogger('features.query-history.local-storage');
|
||||
|
||||
/**
|
||||
* Local storage implementation for Rich History. It keeps all entries in browser's local storage.
|
||||
*/
|
||||
export default class RichHistoryLocalStorage implements RichHistoryStorage {
|
||||
public static getLocalStorageUsageInBytes(): number {
|
||||
const richHistory: RichHistoryLocalStorageDTO[] = store.get(RICH_HISTORY_KEY) || '';
|
||||
// each character is 2 bytes
|
||||
return richHistory.length * 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return history entries based on provided filters, perform migration and clean up entries not matching retention policy.
|
||||
*/
|
||||
@@ -77,21 +86,43 @@ export default class RichHistoryLocalStorage implements RichHistoryStorage {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { queriesToKeep, limitExceeded } = checkLimits(currentRichHistoryDTOs);
|
||||
let { queriesToKeep, limitExceeded } = cleanUpUnstarredQuery(currentRichHistoryDTOs, MAX_HISTORY_ITEMS);
|
||||
|
||||
const updatedHistory: RichHistoryLocalStorageDTO[] = [newRichHistoryQueryDTO, ...queriesToKeep];
|
||||
let updatedHistory: RichHistoryLocalStorageDTO[] = [newRichHistoryQueryDTO, ...queriesToKeep];
|
||||
|
||||
try {
|
||||
store.setObject(RICH_HISTORY_KEY, updatedHistory);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'QuotaExceededError') {
|
||||
throwError(RichHistoryServiceError.StorageFull, `Saving rich history failed: ${error.message}`);
|
||||
} else {
|
||||
throw error;
|
||||
let saveRetriesLeft = 3;
|
||||
let saved = false;
|
||||
|
||||
while (!saved && saveRetriesLeft >= 0) {
|
||||
try {
|
||||
store.setObject(RICH_HISTORY_KEY, updatedHistory);
|
||||
saved = true;
|
||||
} catch (error) {
|
||||
await this.trackLocalStorageUsage('Failed to save rich history to local storage', {
|
||||
saveRetriesLeft: saveRetriesLeft.toString(),
|
||||
quotaExceededError: error instanceof Error && error.name === 'QuotaExceededError' ? 'true' : 'false',
|
||||
errorMessage: error instanceof Error ? error?.message : 'unknown',
|
||||
});
|
||||
|
||||
if (saveRetriesLeft >= 1) {
|
||||
saveRetriesLeft--;
|
||||
const { queriesToKeep: newQueriesToKeep } = cleanUpUnstarredQuery(queriesToKeep, queriesToKeep.length - 1);
|
||||
updatedHistory = [newRichHistoryQueryDTO, ...newQueriesToKeep];
|
||||
queriesToKeep = newQueriesToKeep;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.name === 'QuotaExceededError') {
|
||||
throwError(RichHistoryServiceError.StorageFull, `Saving rich history failed: ${error.message}`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (limitExceeded) {
|
||||
await this.trackLocalStorageUsage('Rich history query limit exceeded.');
|
||||
|
||||
return {
|
||||
warning: {
|
||||
type: RichHistoryStorageWarning.LimitExceeded,
|
||||
@@ -148,6 +179,33 @@ export default class RichHistoryLocalStorage implements RichHistoryStorage {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async trackLocalStorageUsage(message: string, additionalInfo?: Record<string, string>) {
|
||||
const allQueriesCount =
|
||||
(
|
||||
await this.getRichHistory({
|
||||
search: '',
|
||||
sortOrder: SortOrder.Ascending,
|
||||
datasourceFilters: [],
|
||||
starred: false,
|
||||
})
|
||||
).total || -1;
|
||||
|
||||
const allQueriesSizeInBytes = RichHistoryLocalStorage.getLocalStorageUsageInBytes();
|
||||
|
||||
const totalLocalStorageSize = calculateTotalLocalStorageSize();
|
||||
|
||||
const localStats = {
|
||||
totalLocalStorageSize: totalLocalStorageSize?.toString(),
|
||||
allQueriesSizeInBytes: allQueriesSizeInBytes?.toString(),
|
||||
allQueriesCount: allQueriesCount?.toString(),
|
||||
};
|
||||
|
||||
logger.logWarning(message, {
|
||||
...localStats,
|
||||
...additionalInfo,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateRichHistory(
|
||||
@@ -185,17 +243,20 @@ function cleanUp(richHistory: RichHistoryLocalStorageDTO[]): RichHistoryLocalSto
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the entry can be added. Throws an error if current limit has been hit.
|
||||
* Ensures the entry can be added.
|
||||
* Returns queries that should be saved back giving space for one extra query.
|
||||
*/
|
||||
export function checkLimits(queriesToKeep: RichHistoryLocalStorageDTO[]): {
|
||||
export function cleanUpUnstarredQuery(
|
||||
queriesToKeep: RichHistoryLocalStorageDTO[],
|
||||
max: number
|
||||
): {
|
||||
queriesToKeep: RichHistoryLocalStorageDTO[];
|
||||
limitExceeded: boolean;
|
||||
} {
|
||||
// remove oldest non-starred items to give space for the recent query
|
||||
let limitExceeded = false;
|
||||
let current = queriesToKeep.length - 1;
|
||||
while (current >= 0 && queriesToKeep.length >= MAX_HISTORY_ITEMS) {
|
||||
while (current >= 0 && queriesToKeep.length >= max) {
|
||||
if (!queriesToKeep[current].starred) {
|
||||
queriesToKeep.splice(current, 1);
|
||||
limitExceeded = true;
|
||||
@@ -247,3 +308,26 @@ function throwError(name: string, message: string) {
|
||||
error.name = name;
|
||||
throw error;
|
||||
}
|
||||
|
||||
function calculateTotalLocalStorageSize() {
|
||||
try {
|
||||
let total = 0;
|
||||
|
||||
// eslint-disable-next-line
|
||||
const ls = window.localStorage;
|
||||
|
||||
for (let i = 0; i < ls.length; i++) {
|
||||
const key = ls.key(i);
|
||||
if (key) {
|
||||
const value = ls.getItem(key);
|
||||
if (value) {
|
||||
total += key.length + value.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
// each character is 2 bytes
|
||||
return total * 2;
|
||||
} catch (e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export type RichHistorySearchFilters = {
|
||||
// so the resulting timerange from this will be [now - from, now - to].
|
||||
from?: number;
|
||||
to?: number;
|
||||
// true if only starred entries should be returned, false if ALL entries should be returned,
|
||||
starred: boolean;
|
||||
page?: number;
|
||||
};
|
||||
|
||||
@@ -108,7 +108,9 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
|
||||
}),
|
||||
|
||||
grafanaNotifiers: build.query<NotifierDTO[], void>({
|
||||
query: () => ({ url: '/api/alert-notifiers' }),
|
||||
// NOTE: version=2 parameter required for versioned schema (PR #109969)
|
||||
// This parameter will be removed in future when v2 becomes default
|
||||
query: () => ({ url: '/api/alert-notifiers?version=2' }),
|
||||
transformResponse: (response: NotifierDTO[]) => {
|
||||
const populateSecureFieldKey = (
|
||||
option: NotificationChannelOption,
|
||||
@@ -121,11 +123,16 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
|
||||
),
|
||||
});
|
||||
|
||||
// Keep versions array intact for version-specific options lookup
|
||||
// Transform options with secureFieldKey population
|
||||
return response.map((notifier) => ({
|
||||
...notifier,
|
||||
options: notifier.options.map((option) => {
|
||||
return populateSecureFieldKey(option, '');
|
||||
}),
|
||||
options: (notifier.options || []).map((option) => populateSecureFieldKey(option, '')),
|
||||
// Also transform options within each version
|
||||
versions: notifier.versions?.map((version) => ({
|
||||
...version,
|
||||
options: (version.options || []).map((option) => populateSecureFieldKey(option, '')),
|
||||
})),
|
||||
}));
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -46,6 +46,7 @@ export type GrafanaPromRulesOptions = Omit<PromRulesOptions, 'ruleSource' | 'nam
|
||||
state?: PromAlertingRuleState[];
|
||||
title?: string;
|
||||
searchGroupName?: string;
|
||||
searchFolder?: string;
|
||||
type?: 'alerting' | 'recording';
|
||||
ruleMatchers?: string[];
|
||||
plugins?: 'hide' | 'only';
|
||||
@@ -103,6 +104,7 @@ export const prometheusApi = alertingApi.injectEndpoints({
|
||||
title,
|
||||
datasources,
|
||||
searchGroupName,
|
||||
searchFolder,
|
||||
dashboardUid,
|
||||
ruleMatchers,
|
||||
plugins,
|
||||
@@ -123,6 +125,7 @@ export const prometheusApi = alertingApi.injectEndpoints({
|
||||
datasource_uid: datasources,
|
||||
'search.rule_name': title,
|
||||
'search.rule_group': searchGroupName,
|
||||
'search.folder': searchFolder,
|
||||
dashboard_uid: dashboardUid,
|
||||
rule_matcher: ruleMatchers,
|
||||
plugins: plugins,
|
||||
|
||||
@@ -36,6 +36,24 @@ export const ProvisioningAlert = ({ resource, ...rest }: ProvisioningAlertProps)
|
||||
);
|
||||
};
|
||||
|
||||
export const ImportedContactPointAlert = (props: ExtraAlertProps) => {
|
||||
return (
|
||||
<Alert
|
||||
title={t(
|
||||
'alerting.provisioning.title-imported',
|
||||
'This contact point was imported and cannot be edited through the UI'
|
||||
)}
|
||||
severity="info"
|
||||
{...props}
|
||||
>
|
||||
<Trans i18nKey="alerting.provisioning.body-imported">
|
||||
This contact point contains integrations that were imported from an external Alertmanager and is currently
|
||||
read-only. The integrations will become editable after the migration process is complete.
|
||||
</Trans>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProvisioningBadge = ({
|
||||
tooltip,
|
||||
provenance,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'core-js/stable/structured-clone';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
|
||||
import { render } from 'test/test-utils';
|
||||
import { render, screen } from 'test/test-utils';
|
||||
import { byRole, byTestId } from 'testing-library-selector';
|
||||
|
||||
import { grafanaAlertNotifiers } from 'app/features/alerting/unified/mockGrafanaNotifiers';
|
||||
import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext';
|
||||
import { NotifierDTO } from 'app/features/alerting/unified/types/alerting';
|
||||
|
||||
import { ChannelSubForm } from './ChannelSubForm';
|
||||
import { GrafanaCommonChannelSettings } from './GrafanaCommonChannelSettings';
|
||||
@@ -16,6 +17,7 @@ type TestChannelValues = {
|
||||
type: string;
|
||||
settings: Record<string, unknown>;
|
||||
secureFields: Record<string, boolean>;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
type TestReceiverFormValues = {
|
||||
@@ -246,4 +248,241 @@ describe('ChannelSubForm', () => {
|
||||
expect(slackUrl).toBeEnabled();
|
||||
expect(slackUrl).toHaveValue('');
|
||||
});
|
||||
|
||||
describe('version-specific options display', () => {
|
||||
// Create a mock notifier with different options for v0 and v1
|
||||
const legacyOptions = [
|
||||
{
|
||||
element: 'input' as const,
|
||||
inputType: 'text',
|
||||
label: 'Legacy URL',
|
||||
description: 'The legacy endpoint URL',
|
||||
placeholder: '',
|
||||
propertyName: 'legacyUrl',
|
||||
required: true,
|
||||
secure: false,
|
||||
showWhen: { field: '', is: '' },
|
||||
validationRule: '',
|
||||
dependsOn: '',
|
||||
},
|
||||
];
|
||||
|
||||
const webhookWithVersions: NotifierDTO = {
|
||||
...grafanaAlertNotifiers.webhook,
|
||||
versions: [
|
||||
{
|
||||
version: 'v0mimir1',
|
||||
label: 'Webhook (Legacy)',
|
||||
description: 'Legacy webhook from Mimir',
|
||||
canCreate: false,
|
||||
options: legacyOptions,
|
||||
},
|
||||
{
|
||||
version: 'v0mimir2',
|
||||
label: 'Webhook (Legacy v2)',
|
||||
description: 'Legacy webhook v2 from Mimir',
|
||||
canCreate: false,
|
||||
options: legacyOptions,
|
||||
},
|
||||
{
|
||||
version: 'v1',
|
||||
label: 'Webhook',
|
||||
description: 'Sends HTTP POST request',
|
||||
canCreate: true,
|
||||
options: grafanaAlertNotifiers.webhook.options,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const versionedNotifiers: Notifier[] = [
|
||||
{ dto: webhookWithVersions, meta: { enabled: true, order: 1 } },
|
||||
{ dto: grafanaAlertNotifiers.slack, meta: { enabled: true, order: 2 } },
|
||||
];
|
||||
|
||||
function VersionedTestFormWrapper({
|
||||
defaults,
|
||||
initial,
|
||||
}: {
|
||||
defaults: TestChannelValues;
|
||||
initial?: TestChannelValues;
|
||||
}) {
|
||||
const form = useForm<TestReceiverFormValues>({
|
||||
defaultValues: {
|
||||
name: 'test-contact-point',
|
||||
items: [defaults],
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<AlertmanagerProvider accessType="notification">
|
||||
<FormProvider {...form}>
|
||||
<ChannelSubForm
|
||||
defaultValues={defaults}
|
||||
initialValues={initial}
|
||||
pathPrefix={`items.0.`}
|
||||
integrationIndex={0}
|
||||
notifiers={versionedNotifiers}
|
||||
onDuplicate={jest.fn()}
|
||||
commonSettingsComponent={GrafanaCommonChannelSettings}
|
||||
isEditable={true}
|
||||
isTestable={false}
|
||||
canEditProtectedFields={true}
|
||||
/>
|
||||
</FormProvider>
|
||||
</AlertmanagerProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function renderVersionedForm(defaults: TestChannelValues, initial?: TestChannelValues) {
|
||||
return render(<VersionedTestFormWrapper defaults={defaults} initial={initial} />);
|
||||
}
|
||||
|
||||
it('should display v1 options when integration has v1 version', () => {
|
||||
const webhookV1: TestChannelValues = {
|
||||
__id: 'id-0',
|
||||
type: 'webhook',
|
||||
version: 'v1',
|
||||
settings: { url: 'https://example.com' },
|
||||
secureFields: {},
|
||||
};
|
||||
|
||||
renderVersionedForm(webhookV1, webhookV1);
|
||||
|
||||
// Should show v1 URL field (from default options)
|
||||
expect(ui.settings.webhook.url.get()).toBeInTheDocument();
|
||||
// Should NOT show legacy URL field
|
||||
expect(screen.queryByRole('textbox', { name: /Legacy URL/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display v0 options when integration has legacy version', () => {
|
||||
const webhookV0: TestChannelValues = {
|
||||
__id: 'id-0',
|
||||
type: 'webhook',
|
||||
version: 'v0mimir1',
|
||||
settings: { legacyUrl: 'https://legacy.example.com' },
|
||||
secureFields: {},
|
||||
};
|
||||
|
||||
renderVersionedForm(webhookV0, webhookV0);
|
||||
|
||||
// Should show legacy URL field (from v0 options)
|
||||
expect(screen.getByRole('textbox', { name: /Legacy URL/i })).toBeInTheDocument();
|
||||
// Should NOT show v1 URL field
|
||||
expect(ui.settings.webhook.url.query()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display "Legacy" badge for v0mimir1 integration', () => {
|
||||
const webhookV0: TestChannelValues = {
|
||||
__id: 'id-0',
|
||||
type: 'webhook',
|
||||
version: 'v0mimir1',
|
||||
settings: { legacyUrl: 'https://legacy.example.com' },
|
||||
secureFields: {},
|
||||
};
|
||||
|
||||
renderVersionedForm(webhookV0, webhookV0);
|
||||
|
||||
// Should show "Legacy" badge for v0mimir1 integrations
|
||||
expect(screen.getByText('Legacy')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display "Legacy v2" badge for v0mimir2 integration', () => {
|
||||
const webhookV0v2: TestChannelValues = {
|
||||
__id: 'id-0',
|
||||
type: 'webhook',
|
||||
version: 'v0mimir2',
|
||||
settings: { legacyUrl: 'https://legacy.example.com' },
|
||||
secureFields: {},
|
||||
};
|
||||
|
||||
renderVersionedForm(webhookV0v2, webhookV0v2);
|
||||
|
||||
// Should show "Legacy v2" badge for v0mimir2 integrations
|
||||
expect(screen.getByText('Legacy v2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should NOT display version badge for v1 integration', () => {
|
||||
const webhookV1: TestChannelValues = {
|
||||
__id: 'id-0',
|
||||
type: 'webhook',
|
||||
version: 'v1',
|
||||
settings: { url: 'https://example.com' },
|
||||
secureFields: {},
|
||||
};
|
||||
|
||||
renderVersionedForm(webhookV1, webhookV1);
|
||||
|
||||
// Should NOT show version badge for non-legacy v1 integrations
|
||||
expect(screen.queryByText('v1')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should filter out notifiers with canCreate: false from dropdown', () => {
|
||||
// Create a notifier that only has v0 versions (cannot be created)
|
||||
const legacyOnlyNotifier: NotifierDTO = {
|
||||
type: 'wechat',
|
||||
name: 'WeChat',
|
||||
heading: 'WeChat settings',
|
||||
description: 'Sends notifications to WeChat',
|
||||
options: [],
|
||||
versions: [
|
||||
{
|
||||
version: 'v0mimir1',
|
||||
label: 'WeChat (Legacy)',
|
||||
description: 'Legacy WeChat',
|
||||
canCreate: false,
|
||||
options: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const notifiersWithLegacyOnly: Notifier[] = [
|
||||
{ dto: webhookWithVersions, meta: { enabled: true, order: 1 } },
|
||||
{ dto: legacyOnlyNotifier, meta: { enabled: true, order: 2 } },
|
||||
];
|
||||
|
||||
function LegacyOnlyTestWrapper({ defaults }: { defaults: TestChannelValues }) {
|
||||
const form = useForm<TestReceiverFormValues>({
|
||||
defaultValues: {
|
||||
name: 'test-contact-point',
|
||||
items: [defaults],
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<AlertmanagerProvider accessType="notification">
|
||||
<FormProvider {...form}>
|
||||
<ChannelSubForm
|
||||
defaultValues={defaults}
|
||||
pathPrefix={`items.0.`}
|
||||
integrationIndex={0}
|
||||
notifiers={notifiersWithLegacyOnly}
|
||||
onDuplicate={jest.fn()}
|
||||
commonSettingsComponent={GrafanaCommonChannelSettings}
|
||||
isEditable={true}
|
||||
isTestable={false}
|
||||
canEditProtectedFields={true}
|
||||
/>
|
||||
</FormProvider>
|
||||
</AlertmanagerProvider>
|
||||
);
|
||||
}
|
||||
|
||||
render(
|
||||
<LegacyOnlyTestWrapper
|
||||
defaults={{
|
||||
__id: 'id-0',
|
||||
type: 'webhook',
|
||||
settings: {},
|
||||
secureFields: {},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
// Webhook should be in dropdown (has v1 with canCreate: true)
|
||||
expect(ui.typeSelector.get()).toHaveTextContent('Webhook');
|
||||
|
||||
// WeChat should NOT be in the options (only has v0 with canCreate: false)
|
||||
// We can't easily check dropdown options without opening it, but the filter should work
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Controller, FieldErrors, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Alert, Button, Field, Select, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||
import { Alert, Badge, Button, Field, Select, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||
import { NotificationChannelOption } from 'app/features/alerting/unified/types/alerting';
|
||||
|
||||
import {
|
||||
@@ -16,6 +16,12 @@ import {
|
||||
GrafanaChannelValues,
|
||||
ReceiverFormValues,
|
||||
} from '../../../types/receiver-form';
|
||||
import {
|
||||
canCreateNotifier,
|
||||
getLegacyVersionLabel,
|
||||
getOptionsForVersion,
|
||||
isLegacyVersion,
|
||||
} from '../../../utils/notifier-versions';
|
||||
import { OnCallIntegrationType } from '../grafanaAppReceivers/onCall/useOnCallIntegration';
|
||||
|
||||
import { ChannelOptions } from './ChannelOptions';
|
||||
@@ -62,6 +68,7 @@ export function ChannelSubForm<R extends ChannelValues>({
|
||||
|
||||
const channelFieldPath = `items.${integrationIndex}` as const;
|
||||
const typeFieldPath = `${channelFieldPath}.type` as const;
|
||||
const versionFieldPath = `${channelFieldPath}.version` as const;
|
||||
const settingsFieldPath = `${channelFieldPath}.settings` as const;
|
||||
const secureFieldsPath = `${channelFieldPath}.secureFields` as const;
|
||||
|
||||
@@ -104,6 +111,9 @@ export function ChannelSubForm<R extends ChannelValues>({
|
||||
|
||||
setValue(settingsFieldPath, defaultNotifierSettings);
|
||||
setValue(secureFieldsPath, {});
|
||||
|
||||
// Reset version when changing type - backend will use its default
|
||||
setValue(versionFieldPath, undefined);
|
||||
}
|
||||
|
||||
// Restore initial value of an existing oncall integration
|
||||
@@ -123,6 +133,7 @@ export function ChannelSubForm<R extends ChannelValues>({
|
||||
setValue,
|
||||
settingsFieldPath,
|
||||
typeFieldPath,
|
||||
versionFieldPath,
|
||||
secureFieldsPath,
|
||||
getValues,
|
||||
watch,
|
||||
@@ -164,24 +175,30 @@ export function ChannelSubForm<R extends ChannelValues>({
|
||||
setValue(`${settingsFieldPath}.${fieldPath}`, undefined);
|
||||
};
|
||||
|
||||
const typeOptions = useMemo(
|
||||
(): SelectableValue[] =>
|
||||
sortBy(notifiers, ({ dto, meta }) => [meta?.order ?? 0, dto.name]).map<SelectableValue>(
|
||||
({ dto: { name, type }, meta }) => ({
|
||||
// @ts-expect-error ReactNode is supported
|
||||
const typeOptions = useMemo((): SelectableValue[] => {
|
||||
// Filter out notifiers that can't be created (e.g., v0-only integrations like WeChat)
|
||||
// These are legacy integrations that only exist in Mimir and can't be created in Grafana
|
||||
const creatableNotifiers = notifiers.filter(({ dto }) => canCreateNotifier(dto));
|
||||
|
||||
return sortBy(creatableNotifiers, ({ dto, meta }) => [meta?.order ?? 0, dto.name]).map<SelectableValue>(
|
||||
({ dto: { name, type }, meta }) => {
|
||||
return {
|
||||
// ReactNode is supported in Select label, but types don't reflect it
|
||||
/* eslint-disable @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any */
|
||||
label: (
|
||||
<Stack alignItems="center" gap={1}>
|
||||
{name}
|
||||
{meta?.badge}
|
||||
</Stack>
|
||||
),
|
||||
) as any,
|
||||
/* eslint-enable @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any */
|
||||
value: type,
|
||||
description: meta?.description,
|
||||
isDisabled: meta ? !meta.enabled : false,
|
||||
})
|
||||
),
|
||||
[notifiers]
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
}, [notifiers]);
|
||||
|
||||
const handleTest = async () => {
|
||||
await trigger();
|
||||
@@ -198,10 +215,21 @@ export function ChannelSubForm<R extends ChannelValues>({
|
||||
// Cloud AM takes no value at all
|
||||
const isParseModeNone = parse_mode === 'None' || !parse_mode;
|
||||
const showTelegramWarning = isTelegram && !isParseModeNone;
|
||||
|
||||
// Check if current integration is a legacy version (canCreate: false)
|
||||
// Legacy integrations are read-only and cannot be edited
|
||||
// Read version from existing integration data (stored in receiver config)
|
||||
const integrationVersion = initialValues?.version || defaultValues.version;
|
||||
const isLegacy = notifier ? isLegacyVersion(notifier.dto, integrationVersion) : false;
|
||||
|
||||
// Get the correct options based on the integration's version
|
||||
// This ensures legacy (v0) integrations display the correct schema
|
||||
const versionedOptions = notifier ? getOptionsForVersion(notifier.dto, integrationVersion) : [];
|
||||
|
||||
// if there are mandatory options defined, optional options will be hidden by a collapse
|
||||
// if there aren't mandatory options, all options will be shown without collapse
|
||||
const mandatoryOptions = notifier?.dto.options.filter((o) => o.required) ?? [];
|
||||
const optionalOptions = notifier?.dto.options.filter((o) => !o.required) ?? [];
|
||||
const mandatoryOptions = versionedOptions.filter((o) => o.required);
|
||||
const optionalOptions = versionedOptions.filter((o) => !o.required);
|
||||
|
||||
const contactPointTypeInputId = `contact-point-type-${pathPrefix}`;
|
||||
return (
|
||||
@@ -214,21 +242,35 @@ export function ChannelSubForm<R extends ChannelValues>({
|
||||
data-testid={`${pathPrefix}type`}
|
||||
noMargin
|
||||
>
|
||||
<Controller
|
||||
name={typeFieldPath}
|
||||
control={control}
|
||||
defaultValue={defaultValues.type}
|
||||
render={({ field: { ref, onChange, ...field } }) => (
|
||||
<Select
|
||||
disabled={!isEditable}
|
||||
inputId={contactPointTypeInputId}
|
||||
{...field}
|
||||
width={37}
|
||||
options={typeOptions}
|
||||
onChange={(value) => onChange(value?.value)}
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
<Controller
|
||||
name={typeFieldPath}
|
||||
control={control}
|
||||
defaultValue={defaultValues.type}
|
||||
render={({ field: { ref, onChange, ...field } }) => (
|
||||
<Select
|
||||
disabled={!isEditable}
|
||||
inputId={contactPointTypeInputId}
|
||||
{...field}
|
||||
width={37}
|
||||
options={typeOptions}
|
||||
onChange={(value) => onChange(value?.value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{isLegacy && integrationVersion && (
|
||||
<Badge
|
||||
text={getLegacyVersionLabel(integrationVersion)}
|
||||
color="orange"
|
||||
icon="exclamation-triangle"
|
||||
tooltip={t(
|
||||
'alerting.channel-sub-form.tooltip-legacy-version',
|
||||
'This is a legacy integration (version: {{version}}). It cannot be modified.',
|
||||
{ version: integrationVersion }
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
</Field>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
@@ -292,7 +334,7 @@ export function ChannelSubForm<R extends ChannelValues>({
|
||||
name: notifier.dto.name,
|
||||
})}
|
||||
>
|
||||
{notifier.dto.info !== '' && (
|
||||
{notifier.dto.info && (
|
||||
<Alert title="" severity="info">
|
||||
{notifier.dto.info}
|
||||
</Alert>
|
||||
|
||||
@@ -18,12 +18,13 @@ import {
|
||||
|
||||
import { alertmanagerApi } from '../../../api/alertmanagerApi';
|
||||
import { GrafanaChannelValues, ReceiverFormValues } from '../../../types/receiver-form';
|
||||
import { hasLegacyIntegrations } from '../../../utils/notifier-versions';
|
||||
import {
|
||||
formChannelValuesToGrafanaChannelConfig,
|
||||
formValuesToGrafanaReceiver,
|
||||
grafanaReceiverToFormValues,
|
||||
} from '../../../utils/receiver-form';
|
||||
import { ProvisionedResource, ProvisioningAlert } from '../../Provisioning';
|
||||
import { ImportedContactPointAlert, ProvisionedResource, ProvisioningAlert } from '../../Provisioning';
|
||||
import { ReceiverTypes } from '../grafanaAppReceivers/onCall/onCall';
|
||||
import { useOnCallIntegration } from '../grafanaAppReceivers/onCall/useOnCallIntegration';
|
||||
|
||||
@@ -39,6 +40,8 @@ const defaultChannelValues: GrafanaChannelValues = Object.freeze({
|
||||
secureFields: {},
|
||||
disableResolveMessage: false,
|
||||
type: 'email',
|
||||
// version is intentionally not set here - it will be determined by the notifier's currentVersion
|
||||
// when the integration is created/type is changed. The backend will use its default if not provided.
|
||||
});
|
||||
|
||||
interface Props {
|
||||
@@ -67,7 +70,6 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
|
||||
} = useOnCallIntegration();
|
||||
|
||||
const { data: grafanaNotifiers = [], isLoading: isLoadingNotifiers } = useGrafanaNotifiersQuery();
|
||||
|
||||
const [testReceivers, setTestReceivers] = useState<Receiver[]>();
|
||||
|
||||
// transform receiver DTO to form values
|
||||
@@ -135,15 +137,20 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
|
||||
);
|
||||
}
|
||||
|
||||
// Map notifiers to Notifier[] format for ReceiverForm
|
||||
// The grafanaNotifiers include version-specific options via the versions array from the backend
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions
|
||||
const notifiers: Notifier[] = grafanaNotifiers.map((n) => {
|
||||
if (n.type === ReceiverTypes.OnCall) {
|
||||
return {
|
||||
dto: extendOnCallNotifierFeatures(n),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions
|
||||
dto: extendOnCallNotifierFeatures(n as any) as any,
|
||||
meta: onCallNotifierMeta,
|
||||
};
|
||||
}
|
||||
|
||||
return { dto: n };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions
|
||||
return { dto: n as any };
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -163,7 +170,12 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{contactPoint?.provisioned && <ProvisioningAlert resource={ProvisionedResource.ContactPoint} />}
|
||||
{contactPoint?.provisioned && hasLegacyIntegrations(contactPoint, grafanaNotifiers) && (
|
||||
<ImportedContactPointAlert />
|
||||
)}
|
||||
{contactPoint?.provisioned && !hasLegacyIntegrations(contactPoint, grafanaNotifiers) && (
|
||||
<ProvisioningAlert resource={ProvisionedResource.ContactPoint} />
|
||||
)}
|
||||
|
||||
<ReceiverForm<GrafanaChannelValues>
|
||||
contactPointId={contactPoint?.id}
|
||||
|
||||
@@ -455,6 +455,25 @@ describe('grafana-managed rules', () => {
|
||||
expect(frontendFilter.ruleMatches(regularRule)).toBe(true);
|
||||
expect(frontendFilter.ruleMatches(pluginRule)).toBe(true);
|
||||
});
|
||||
|
||||
it('should include searchFolder in backend filter when namespace is provided', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({ namespace: 'my-folder' }));
|
||||
|
||||
expect(backendFilter.searchFolder).toBe('my-folder');
|
||||
});
|
||||
|
||||
it('should skip namespace filtering on frontend when backend filtering is enabled', () => {
|
||||
const group: PromRuleGroupDTO = {
|
||||
name: 'Test Group',
|
||||
file: 'production/alerts',
|
||||
rules: [],
|
||||
interval: 60,
|
||||
};
|
||||
|
||||
const { frontendFilter } = getGrafanaFilter(getFilter({ namespace: 'staging' }));
|
||||
// Should return true because namespace filter is null (handled by backend)
|
||||
expect(frontendFilter.groupMatches(group)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when alertingUIUseBackendFilters is disabled', () => {
|
||||
@@ -537,6 +556,12 @@ describe('grafana-managed rules', () => {
|
||||
expect(backendFilter.searchGroupName).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not include searchFolder in backend filter', () => {
|
||||
const { backendFilter } = getGrafanaFilter(getFilter({ namespace: 'my-folder' }));
|
||||
|
||||
expect(backendFilter.searchFolder).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should perform groupName filtering on frontend', () => {
|
||||
const group: PromRuleGroupDTO = {
|
||||
name: 'CPU Usage Alerts',
|
||||
@@ -706,8 +731,8 @@ describe('grafana-managed rules', () => {
|
||||
expect(frontendFilter.groupMatches(group)).toBe(true);
|
||||
});
|
||||
|
||||
it('should still apply always-frontend filters (namespace)', () => {
|
||||
// Namespace filter should still work
|
||||
it('should skip namespace filtering on frontend', () => {
|
||||
// Namespace filter should be handled by backend
|
||||
const group: PromRuleGroupDTO = {
|
||||
name: 'Test Group',
|
||||
file: 'production/alerts',
|
||||
@@ -719,7 +744,7 @@ describe('grafana-managed rules', () => {
|
||||
expect(nsFilter.groupMatches(group)).toBe(true);
|
||||
|
||||
const { frontendFilter: nsFilter2 } = getGrafanaFilter(getFilter({ namespace: 'staging' }));
|
||||
expect(nsFilter2.groupMatches(group)).toBe(false);
|
||||
expect(nsFilter2.groupMatches(group)).toBe(true);
|
||||
});
|
||||
|
||||
it('should skip dataSourceNames filtering on frontend (handled by backend)', () => {
|
||||
@@ -807,8 +832,8 @@ describe('grafana-managed rules', () => {
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for client-side only filters', () => {
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true);
|
||||
it('should return false for namespace filter (handled by backend)', () => {
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for plugins filter (handled by backend when feature toggle is enabled)', () => {
|
||||
@@ -862,8 +887,8 @@ describe('grafana-managed rules', () => {
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false);
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false);
|
||||
|
||||
// Should return true for: always-frontend filters only (namespace)
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true);
|
||||
// Should return false for: namespace (handled by backend)
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ namespace: 'production' }))).toBe(false);
|
||||
|
||||
// plugins is backend-handled when both feature toggles are enabled
|
||||
expect(hasGrafanaClientSideFilters(getFilter({ plugins: 'hide' }))).toBe(false);
|
||||
|
||||
@@ -96,6 +96,7 @@ export function getGrafanaFilter(filterState: Partial<RulesFilter>) {
|
||||
datasources: ruleFilterConfig.dataSourceNames ? undefined : datasourceUids,
|
||||
ruleMatchers: ruleMatchersBackendFilter,
|
||||
plugins: ruleFilterConfig.plugins ? undefined : normalizedFilterState.plugins,
|
||||
searchFolder: groupFilterConfig.namespace ? undefined : normalizedFilterState.namespace,
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -134,7 +135,7 @@ function buildGrafanaFilterConfigs() {
|
||||
};
|
||||
|
||||
const groupFilterConfig: GroupFilterConfig = {
|
||||
namespace: namespaceFilter,
|
||||
namespace: useBackendFilters ? null : namespaceFilter,
|
||||
groupName: useBackendFilters ? null : groupNameFilter,
|
||||
};
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ interface GrafanaPromApiFilter {
|
||||
contactPoint?: string;
|
||||
title?: string;
|
||||
searchGroupName?: string;
|
||||
searchFolder?: string;
|
||||
type?: 'alerting' | 'recording';
|
||||
dashboardUid?: string;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ describe('paginationLimits', () => {
|
||||
{ contactPoint: 'slack' },
|
||||
{ dataSourceNames: ['prometheus'] },
|
||||
{ labels: ['severity=critical'] },
|
||||
{ namespace: 'production' },
|
||||
])(
|
||||
'should return rule limit for grafana + large limit for datasource when only backend filters are used: %p',
|
||||
(filterState) => {
|
||||
@@ -84,16 +85,6 @@ describe('paginationLimits', () => {
|
||||
expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE });
|
||||
}
|
||||
);
|
||||
|
||||
it.each<Partial<RulesFilter>>([
|
||||
{ namespace: 'production' },
|
||||
{ ruleState: PromAlertingRuleState.Firing, namespace: 'production' },
|
||||
])('should return large limits for both when frontend filters are used: %p', (filterState) => {
|
||||
const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState));
|
||||
|
||||
expect(grafanaManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE });
|
||||
expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when alertingUIUseFullyCompatBackendFilters is enabled', () => {
|
||||
@@ -158,6 +149,7 @@ describe('paginationLimits', () => {
|
||||
{ contactPoint: 'slack' },
|
||||
{ dataSourceNames: ['prometheus'] },
|
||||
{ labels: ['severity=critical'] },
|
||||
{ namespace: 'production' },
|
||||
])(
|
||||
'should return rule limit for grafana + large limit for datasource when only backend filters are used: %p',
|
||||
(filterState) => {
|
||||
@@ -167,16 +159,6 @@ describe('paginationLimits', () => {
|
||||
expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE });
|
||||
}
|
||||
);
|
||||
|
||||
it.each<Partial<RulesFilter>>([{ namespace: 'production' }])(
|
||||
'should return large limits for both when frontend filters are used: %p',
|
||||
(filterState) => {
|
||||
const { grafanaManagedLimit, datasourceManagedLimit } = getFilteredRulesLimits(getFilter(filterState));
|
||||
|
||||
expect(grafanaManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE });
|
||||
expect(datasourceManagedLimit).toEqual({ groupLimit: FILTERED_GROUPS_LARGE_API_PAGE_SIZE });
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,6 +80,20 @@ export type CloudNotifierType =
|
||||
| 'jira';
|
||||
|
||||
export type NotifierType = GrafanaNotifierType | CloudNotifierType;
|
||||
|
||||
/**
|
||||
* Represents a specific version of a notifier integration
|
||||
* Used for integration versioning during Single Alert Manager migration
|
||||
*/
|
||||
export interface NotifierVersion {
|
||||
version: string;
|
||||
label: string;
|
||||
description: string;
|
||||
options: NotificationChannelOption[];
|
||||
/** Whether this version can be used to create new integrations */
|
||||
canCreate?: boolean;
|
||||
}
|
||||
|
||||
export interface NotifierDTO<T = NotifierType> {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -88,6 +102,23 @@ export interface NotifierDTO<T = NotifierType> {
|
||||
options: NotificationChannelOption[];
|
||||
info?: string;
|
||||
secure?: boolean;
|
||||
/**
|
||||
* Available versions for this notifier from the backend
|
||||
* Each version contains version-specific options and metadata
|
||||
*/
|
||||
versions?: NotifierVersion[];
|
||||
/**
|
||||
* The default version that the backend will use when creating new integrations.
|
||||
* Returned by the backend from /api/alert-notifiers?version=2
|
||||
*
|
||||
* - "v1" for most notifiers (modern Grafana version)
|
||||
* - "v0mimir1" for legacy-only notifiers (e.g., WeChat)
|
||||
*
|
||||
* Note: Currently not used in the frontend. The backend handles version
|
||||
* selection automatically. Could be used in the future to display
|
||||
* version information or validate notifier capabilities.
|
||||
*/
|
||||
currentVersion?: string;
|
||||
}
|
||||
|
||||
export interface NotificationChannelType {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ControlledField } from '../hooks/useControlledFieldArray';
|
||||
export interface ChannelValues {
|
||||
__id: string; // used to correlate form values to original DTOs
|
||||
type: string;
|
||||
version?: string; // Integration version (e.g. "v0" for Mimir legacy, "v1" for Grafana)
|
||||
settings: Record<string, any>;
|
||||
secureFields: Record<string, boolean | ''>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,429 @@
|
||||
import { GrafanaManagedContactPoint } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { NotificationChannelOption, NotifierDTO, NotifierVersion } from '../types/alerting';
|
||||
|
||||
import {
|
||||
canCreateNotifier,
|
||||
getLegacyVersionLabel,
|
||||
getOptionsForVersion,
|
||||
hasLegacyIntegrations,
|
||||
isLegacyVersion,
|
||||
} from './notifier-versions';
|
||||
|
||||
// Helper to create a minimal NotifierDTO for testing
|
||||
function createNotifier(overrides: Partial<NotifierDTO> = {}): NotifierDTO {
|
||||
return {
|
||||
name: 'Test Notifier',
|
||||
description: 'Test description',
|
||||
type: 'webhook',
|
||||
heading: 'Test heading',
|
||||
options: [
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'text',
|
||||
label: 'Default Option',
|
||||
description: 'Default option description',
|
||||
placeholder: '',
|
||||
propertyName: 'defaultOption',
|
||||
required: true,
|
||||
secure: false,
|
||||
showWhen: { field: '', is: '' },
|
||||
validationRule: '',
|
||||
dependsOn: '',
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to create a NotifierVersion for testing
|
||||
function createVersion(overrides: Partial<NotifierVersion> = {}): NotifierVersion {
|
||||
return {
|
||||
version: 'v1',
|
||||
label: 'Test Version',
|
||||
description: 'Test version description',
|
||||
options: [
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'text',
|
||||
label: 'Version Option',
|
||||
description: 'Version option description',
|
||||
placeholder: '',
|
||||
propertyName: 'versionOption',
|
||||
required: true,
|
||||
secure: false,
|
||||
showWhen: { field: '', is: '' },
|
||||
validationRule: '',
|
||||
dependsOn: '',
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('notifier-versions utilities', () => {
|
||||
describe('canCreateNotifier', () => {
|
||||
it('should return true if notifier has no versions array', () => {
|
||||
const notifier = createNotifier({ versions: undefined });
|
||||
expect(canCreateNotifier(notifier)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if notifier has empty versions array', () => {
|
||||
const notifier = createNotifier({ versions: [] });
|
||||
expect(canCreateNotifier(notifier)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if at least one version has canCreate: true', () => {
|
||||
const notifier = createNotifier({
|
||||
versions: [
|
||||
createVersion({ version: 'v0mimir1', canCreate: false }),
|
||||
createVersion({ version: 'v1', canCreate: true }),
|
||||
],
|
||||
});
|
||||
expect(canCreateNotifier(notifier)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if at least one version has canCreate: undefined (defaults to true)', () => {
|
||||
const notifier = createNotifier({
|
||||
versions: [
|
||||
createVersion({ version: 'v0mimir1', canCreate: false }),
|
||||
createVersion({ version: 'v1', canCreate: undefined }),
|
||||
],
|
||||
});
|
||||
expect(canCreateNotifier(notifier)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if all versions have canCreate: false', () => {
|
||||
const notifier = createNotifier({
|
||||
versions: [
|
||||
createVersion({ version: 'v0mimir1', canCreate: false }),
|
||||
createVersion({ version: 'v0mimir2', canCreate: false }),
|
||||
],
|
||||
});
|
||||
expect(canCreateNotifier(notifier)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for notifiers like WeChat that only have legacy versions', () => {
|
||||
const wechatNotifier = createNotifier({
|
||||
name: 'WeChat',
|
||||
type: 'wechat',
|
||||
versions: [createVersion({ version: 'v0mimir1', canCreate: false })],
|
||||
});
|
||||
expect(canCreateNotifier(wechatNotifier)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLegacyVersion', () => {
|
||||
it('should return false if no version is specified', () => {
|
||||
const notifier = createNotifier({
|
||||
versions: [createVersion({ version: 'v0mimir1', canCreate: false })],
|
||||
});
|
||||
expect(isLegacyVersion(notifier, undefined)).toBe(false);
|
||||
expect(isLegacyVersion(notifier, '')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if notifier has no versions array', () => {
|
||||
const notifier = createNotifier({ versions: undefined });
|
||||
expect(isLegacyVersion(notifier, 'v0mimir1')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if notifier has empty versions array', () => {
|
||||
const notifier = createNotifier({ versions: [] });
|
||||
expect(isLegacyVersion(notifier, 'v0mimir1')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if version is not found in versions array', () => {
|
||||
const notifier = createNotifier({
|
||||
versions: [createVersion({ version: 'v1', canCreate: true })],
|
||||
});
|
||||
expect(isLegacyVersion(notifier, 'v0mimir1')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if version has canCreate: true', () => {
|
||||
const notifier = createNotifier({
|
||||
versions: [createVersion({ version: 'v1', canCreate: true })],
|
||||
});
|
||||
expect(isLegacyVersion(notifier, 'v1')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if version has canCreate: undefined', () => {
|
||||
const notifier = createNotifier({
|
||||
versions: [createVersion({ version: 'v1', canCreate: undefined })],
|
||||
});
|
||||
expect(isLegacyVersion(notifier, 'v1')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if version has canCreate: false', () => {
|
||||
const notifier = createNotifier({
|
||||
versions: [
|
||||
createVersion({ version: 'v0mimir1', canCreate: false }),
|
||||
createVersion({ version: 'v1', canCreate: true }),
|
||||
],
|
||||
});
|
||||
expect(isLegacyVersion(notifier, 'v0mimir1')).toBe(true);
|
||||
});
|
||||
|
||||
it('should correctly identify legacy versions in a mixed notifier', () => {
|
||||
const notifier = createNotifier({
|
||||
versions: [
|
||||
createVersion({ version: 'v0mimir1', canCreate: false }),
|
||||
createVersion({ version: 'v0mimir2', canCreate: false }),
|
||||
createVersion({ version: 'v1', canCreate: true }),
|
||||
],
|
||||
});
|
||||
expect(isLegacyVersion(notifier, 'v0mimir1')).toBe(true);
|
||||
expect(isLegacyVersion(notifier, 'v0mimir2')).toBe(true);
|
||||
expect(isLegacyVersion(notifier, 'v1')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOptionsForVersion', () => {
|
||||
const defaultOptions: NotificationChannelOption[] = [
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'text',
|
||||
label: 'Default URL',
|
||||
description: 'Default URL description',
|
||||
placeholder: '',
|
||||
propertyName: 'url',
|
||||
required: true,
|
||||
secure: false,
|
||||
showWhen: { field: '', is: '' },
|
||||
validationRule: '',
|
||||
dependsOn: '',
|
||||
},
|
||||
];
|
||||
|
||||
const v0Options: NotificationChannelOption[] = [
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'text',
|
||||
label: 'Legacy URL',
|
||||
description: 'Legacy URL description',
|
||||
placeholder: '',
|
||||
propertyName: 'legacyUrl',
|
||||
required: true,
|
||||
secure: false,
|
||||
showWhen: { field: '', is: '' },
|
||||
validationRule: '',
|
||||
dependsOn: '',
|
||||
},
|
||||
];
|
||||
|
||||
const v1Options: NotificationChannelOption[] = [
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'text',
|
||||
label: 'Modern URL',
|
||||
description: 'Modern URL description',
|
||||
placeholder: '',
|
||||
propertyName: 'modernUrl',
|
||||
required: true,
|
||||
secure: false,
|
||||
showWhen: { field: '', is: '' },
|
||||
validationRule: '',
|
||||
dependsOn: '',
|
||||
},
|
||||
];
|
||||
|
||||
it('should return options from default creatable version if no version is specified', () => {
|
||||
const notifier = createNotifier({
|
||||
options: defaultOptions,
|
||||
versions: [createVersion({ version: 'v1', options: v1Options, canCreate: true })],
|
||||
});
|
||||
// When no version specified, should use options from the default creatable version
|
||||
expect(getOptionsForVersion(notifier, undefined)).toBe(v1Options);
|
||||
});
|
||||
|
||||
it('should return default options if no version is specified and empty string is passed', () => {
|
||||
const notifier = createNotifier({
|
||||
options: defaultOptions,
|
||||
versions: [createVersion({ version: 'v1', options: v1Options, canCreate: true })],
|
||||
});
|
||||
// Empty string is still a falsy version, so should use default creatable version
|
||||
expect(getOptionsForVersion(notifier, '')).toBe(v1Options);
|
||||
});
|
||||
|
||||
it('should return default options if notifier has no versions array', () => {
|
||||
const notifier = createNotifier({
|
||||
options: defaultOptions,
|
||||
versions: undefined,
|
||||
});
|
||||
expect(getOptionsForVersion(notifier, 'v1')).toBe(defaultOptions);
|
||||
});
|
||||
|
||||
it('should return default options if notifier has empty versions array', () => {
|
||||
const notifier = createNotifier({
|
||||
options: defaultOptions,
|
||||
versions: [],
|
||||
});
|
||||
expect(getOptionsForVersion(notifier, 'v1')).toBe(defaultOptions);
|
||||
});
|
||||
|
||||
it('should return default options if version is not found', () => {
|
||||
const notifier = createNotifier({
|
||||
options: defaultOptions,
|
||||
versions: [createVersion({ version: 'v1', options: v1Options })],
|
||||
});
|
||||
expect(getOptionsForVersion(notifier, 'v0mimir1')).toBe(defaultOptions);
|
||||
});
|
||||
|
||||
it('should return version-specific options when version is found', () => {
|
||||
const notifier = createNotifier({
|
||||
options: defaultOptions,
|
||||
versions: [
|
||||
createVersion({ version: 'v0mimir1', options: v0Options }),
|
||||
createVersion({ version: 'v1', options: v1Options }),
|
||||
],
|
||||
});
|
||||
expect(getOptionsForVersion(notifier, 'v0mimir1')).toBe(v0Options);
|
||||
expect(getOptionsForVersion(notifier, 'v1')).toBe(v1Options);
|
||||
});
|
||||
|
||||
it('should return default options if version found but has no options', () => {
|
||||
const notifier = createNotifier({
|
||||
options: defaultOptions,
|
||||
versions: [
|
||||
{
|
||||
version: 'v1',
|
||||
label: 'V1',
|
||||
description: 'V1 description',
|
||||
options: undefined as unknown as NotificationChannelOption[],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(getOptionsForVersion(notifier, 'v1')).toBe(defaultOptions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasLegacyIntegrations', () => {
|
||||
// Helper to create a minimal contact point for testing
|
||||
function createContactPoint(overrides: Partial<GrafanaManagedContactPoint> = {}): GrafanaManagedContactPoint {
|
||||
return {
|
||||
name: 'Test Contact Point',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// Create notifiers with version info for testing
|
||||
const notifiersWithVersions: NotifierDTO[] = [
|
||||
createNotifier({
|
||||
type: 'slack',
|
||||
versions: [
|
||||
createVersion({ version: 'v0mimir1', canCreate: false }),
|
||||
createVersion({ version: 'v1', canCreate: true }),
|
||||
],
|
||||
}),
|
||||
createNotifier({
|
||||
type: 'webhook',
|
||||
versions: [
|
||||
createVersion({ version: 'v0mimir1', canCreate: false }),
|
||||
createVersion({ version: 'v0mimir2', canCreate: false }),
|
||||
createVersion({ version: 'v1', canCreate: true }),
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
it('should return false if contact point is undefined', () => {
|
||||
expect(hasLegacyIntegrations(undefined, notifiersWithVersions)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if notifiers is undefined', () => {
|
||||
const contactPoint = createContactPoint({
|
||||
grafana_managed_receiver_configs: [{ type: 'slack', settings: {}, version: 'v0mimir1' }],
|
||||
});
|
||||
expect(hasLegacyIntegrations(contactPoint, undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if contact point has no integrations', () => {
|
||||
const contactPoint = createContactPoint({ grafana_managed_receiver_configs: undefined });
|
||||
expect(hasLegacyIntegrations(contactPoint, notifiersWithVersions)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if contact point has empty integrations array', () => {
|
||||
const contactPoint = createContactPoint({ grafana_managed_receiver_configs: [] });
|
||||
expect(hasLegacyIntegrations(contactPoint, notifiersWithVersions)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if all integrations have v1 version (canCreate: true)', () => {
|
||||
const contactPoint = createContactPoint({
|
||||
grafana_managed_receiver_configs: [
|
||||
{ type: 'slack', settings: {}, version: 'v1' },
|
||||
{ type: 'webhook', settings: {}, version: 'v1' },
|
||||
],
|
||||
});
|
||||
expect(hasLegacyIntegrations(contactPoint, notifiersWithVersions)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if all integrations have no version', () => {
|
||||
const contactPoint = createContactPoint({
|
||||
grafana_managed_receiver_configs: [
|
||||
{ type: 'slack', settings: {} },
|
||||
{ type: 'webhook', settings: {} },
|
||||
],
|
||||
});
|
||||
expect(hasLegacyIntegrations(contactPoint, notifiersWithVersions)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if any integration has a legacy version (canCreate: false)', () => {
|
||||
const contactPoint = createContactPoint({
|
||||
grafana_managed_receiver_configs: [
|
||||
{ type: 'slack', settings: {}, version: 'v0mimir1' },
|
||||
{ type: 'webhook', settings: {}, version: 'v1' },
|
||||
],
|
||||
});
|
||||
expect(hasLegacyIntegrations(contactPoint, notifiersWithVersions)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if all integrations have legacy versions', () => {
|
||||
const contactPoint = createContactPoint({
|
||||
grafana_managed_receiver_configs: [
|
||||
{ type: 'slack', settings: {}, version: 'v0mimir1' },
|
||||
{ type: 'webhook', settings: {}, version: 'v0mimir2' },
|
||||
],
|
||||
});
|
||||
expect(hasLegacyIntegrations(contactPoint, notifiersWithVersions)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if notifier type is not found in notifiers array', () => {
|
||||
const contactPoint = createContactPoint({
|
||||
grafana_managed_receiver_configs: [{ type: 'unknown', settings: {}, version: 'v0mimir1' }],
|
||||
});
|
||||
expect(hasLegacyIntegrations(contactPoint, notifiersWithVersions)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLegacyVersionLabel', () => {
|
||||
it('should return "Legacy" for undefined version', () => {
|
||||
expect(getLegacyVersionLabel(undefined)).toBe('Legacy');
|
||||
});
|
||||
|
||||
it('should return "Legacy" for empty string version', () => {
|
||||
expect(getLegacyVersionLabel('')).toBe('Legacy');
|
||||
});
|
||||
|
||||
it('should return "Legacy" for v0mimir1', () => {
|
||||
expect(getLegacyVersionLabel('v0mimir1')).toBe('Legacy');
|
||||
});
|
||||
|
||||
it('should return "Legacy v2" for v0mimir2', () => {
|
||||
expect(getLegacyVersionLabel('v0mimir2')).toBe('Legacy v2');
|
||||
});
|
||||
|
||||
it('should return "Legacy v3" for v0mimir3', () => {
|
||||
expect(getLegacyVersionLabel('v0mimir3')).toBe('Legacy v3');
|
||||
});
|
||||
|
||||
it('should return "Legacy" for v1 (trailing 1)', () => {
|
||||
expect(getLegacyVersionLabel('v1')).toBe('Legacy');
|
||||
});
|
||||
|
||||
it('should return "Legacy v2" for v2 (trailing 2)', () => {
|
||||
expect(getLegacyVersionLabel('v2')).toBe('Legacy v2');
|
||||
});
|
||||
|
||||
it('should return "Legacy" for version strings without trailing number', () => {
|
||||
expect(getLegacyVersionLabel('legacy')).toBe('Legacy');
|
||||
});
|
||||
});
|
||||
});
|
||||
126
public/app/features/alerting/unified/utils/notifier-versions.ts
Normal file
126
public/app/features/alerting/unified/utils/notifier-versions.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Utilities for integration versioning
|
||||
*
|
||||
* These utilities help get version-specific options from the backend response
|
||||
* (via /api/alert-notifiers?version=2)
|
||||
*/
|
||||
|
||||
import { GrafanaManagedContactPoint } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { NotificationChannelOption, NotifierDTO } from '../types/alerting';
|
||||
|
||||
/**
|
||||
* Checks if a notifier can be used to create new integrations.
|
||||
* A notifier can be created if it has at least one version with canCreate: true,
|
||||
* or if it has no versions array (legacy behavior).
|
||||
*
|
||||
* @param notifier - The notifier DTO to check
|
||||
* @returns True if the notifier can be used to create new integrations
|
||||
*/
|
||||
export function canCreateNotifier(notifier: NotifierDTO): boolean {
|
||||
// If no versions array, assume it can be created (legacy behavior)
|
||||
if (!notifier.versions || notifier.versions.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if any version has canCreate: true (or undefined, which defaults to true)
|
||||
return notifier.versions.some((v) => v.canCreate !== false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific version is legacy (cannot be created).
|
||||
* A version is legacy if it has canCreate: false in the notifier's versions array.
|
||||
*
|
||||
* @param notifier - The notifier DTO containing versions array
|
||||
* @param version - The version string to check (e.g., 'v0mimir1', 'v1')
|
||||
* @returns True if the version is legacy (canCreate: false)
|
||||
*/
|
||||
export function isLegacyVersion(notifier: NotifierDTO, version?: string): boolean {
|
||||
// If no version specified or no versions array, it's not legacy
|
||||
if (!version || !notifier.versions || notifier.versions.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find the matching version and check its canCreate property
|
||||
const versionData = notifier.versions.find((v) => v.version === version);
|
||||
|
||||
// A version is legacy if canCreate is explicitly false
|
||||
return versionData?.canCreate === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the options for a specific version of a notifier.
|
||||
* Used to display the correct form fields based on integration version.
|
||||
*
|
||||
* @param notifier - The notifier DTO containing versions array
|
||||
* @param version - The version to get options for (e.g., 'v0', 'v1')
|
||||
* @returns The options for the specified version, or default options if version not found
|
||||
*/
|
||||
export function getOptionsForVersion(notifier: NotifierDTO, version?: string): NotificationChannelOption[] {
|
||||
// If no versions array, use default options
|
||||
if (!notifier.versions || notifier.versions.length === 0) {
|
||||
return notifier.options;
|
||||
}
|
||||
|
||||
// If version is specified, find the matching version
|
||||
if (version) {
|
||||
const versionData = notifier.versions.find((v) => v.version === version);
|
||||
// Return version-specific options if found, otherwise fall back to default
|
||||
return versionData?.options ?? notifier.options;
|
||||
}
|
||||
|
||||
// If no version specified, find the default creatable version (canCreate !== false)
|
||||
const defaultVersion = notifier.versions.find((v) => v.canCreate !== false);
|
||||
return defaultVersion?.options ?? notifier.options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a contact point has any legacy (imported) integrations.
|
||||
* A contact point has legacy integrations if any of its integrations uses a version
|
||||
* with canCreate: false in the corresponding notifier's versions array.
|
||||
*
|
||||
* @param contactPoint - The contact point to check
|
||||
* @param notifiers - Array of notifier DTOs to look up version info
|
||||
* @returns True if the contact point has at least one legacy/imported integration
|
||||
*/
|
||||
export function hasLegacyIntegrations(contactPoint?: GrafanaManagedContactPoint, notifiers?: NotifierDTO[]): boolean {
|
||||
if (!contactPoint?.grafana_managed_receiver_configs || !notifiers) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return contactPoint.grafana_managed_receiver_configs.some((config) => {
|
||||
const notifier = notifiers.find((n) => n.type === config.type);
|
||||
return notifier ? isLegacyVersion(notifier, config.version) : false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a user-friendly label for a legacy version.
|
||||
* Extracts the version number from the version string and formats it as:
|
||||
* - "Legacy" for version 1 (e.g., v0mimir1)
|
||||
* - "Legacy v2" for version 2 (e.g., v0mimir2)
|
||||
* - etc.
|
||||
*
|
||||
* Precondition: This function assumes the version is already known to be legacy
|
||||
* (i.e., canCreate: false). Use isLegacyVersion() to check before calling this.
|
||||
*
|
||||
* @param version - The version string (e.g., 'v0mimir1', 'v0mimir2')
|
||||
* @returns A user-friendly label like "Legacy" or "Legacy v2"
|
||||
*/
|
||||
export function getLegacyVersionLabel(version?: string): string {
|
||||
if (!version) {
|
||||
return 'Legacy';
|
||||
}
|
||||
|
||||
// Extract trailing number from version string (e.g., v0mimir1 → 1, v0mimir2 → 2)
|
||||
const match = version.match(/(\d+)$/);
|
||||
if (match) {
|
||||
const num = parseInt(match[1], 10);
|
||||
if (num === 1) {
|
||||
return 'Legacy';
|
||||
}
|
||||
return `Legacy v${num}`;
|
||||
}
|
||||
|
||||
return 'Legacy';
|
||||
}
|
||||
@@ -185,6 +185,7 @@ function grafanaChannelConfigToFormChannelValues(
|
||||
const values: GrafanaChannelValues = {
|
||||
__id: id,
|
||||
type: channel.type as NotifierType,
|
||||
version: channel.version,
|
||||
provenance: channel.provenance,
|
||||
settings: { ...channel.settings },
|
||||
secureFields: { ...channel.secureFields },
|
||||
@@ -239,6 +240,7 @@ export function formChannelValuesToGrafanaChannelConfig(
|
||||
}),
|
||||
secureFields: secureFieldsFromValues,
|
||||
type: values.type,
|
||||
version: values.version ?? existing?.version,
|
||||
name,
|
||||
disableResolveMessage:
|
||||
values.disableResolveMessage ?? existing?.disableResolveMessage ?? defaults.disableResolveMessage,
|
||||
|
||||
@@ -200,7 +200,7 @@ describe('Explore: Query History', () => {
|
||||
await waitForExplore();
|
||||
await openQueryHistory();
|
||||
|
||||
jest.spyOn(localStorage, 'checkLimits').mockImplementationOnce((queries) => {
|
||||
jest.spyOn(localStorage, 'cleanUpUnstarredQuery').mockImplementationOnce((queries) => {
|
||||
return { queriesToKeep: queries, limitExceeded: true };
|
||||
});
|
||||
|
||||
|
||||
@@ -141,6 +141,7 @@ export const getPluginExtensions: GetExtensions = ({
|
||||
description: overrides?.description || addedLink.description || '',
|
||||
path: isString(path) ? getLinkExtensionPathWithTracking(pluginId, path, extensionPointId) : undefined,
|
||||
category: overrides?.category || addedLink.category,
|
||||
openInNewTab: overrides?.openInNewTab ?? addedLink.openInNewTab,
|
||||
};
|
||||
|
||||
extensions.push(extension);
|
||||
|
||||
@@ -420,6 +420,7 @@ export function createExtensionSubMenu(extensions: PluginExtensionLink[]): Panel
|
||||
href: extension.path,
|
||||
onClick: extension.onClick,
|
||||
iconClassName: extension.icon,
|
||||
target: extension.openInNewTab ? '_blank' : undefined,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -433,6 +434,7 @@ export function createExtensionSubMenu(extensions: PluginExtensionLink[]): Panel
|
||||
href: extension.path,
|
||||
onClick: extension.onClick,
|
||||
iconClassName: extension.icon,
|
||||
target: extension.openInNewTab ? '_blank' : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FeatureToggles } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { RepositoryViewList } from 'app/api/clients/provisioning/v0alpha1';
|
||||
|
||||
export const requiredFeatureToggles: Array<keyof FeatureToggles> = ['provisioning', 'kubernetesDashboards'];
|
||||
export const requiredFeatureToggles: Array<keyof FeatureToggles> = ['kubernetesDashboards'];
|
||||
|
||||
/**
|
||||
* Checks if all required feature toggles are enabled
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
|
||||
import { RouteDescriptor } from 'app/core/navigation/types';
|
||||
import { DashboardRoutes } from 'app/types/dashboard';
|
||||
@@ -6,6 +7,11 @@ import { checkRequiredFeatures } from '../GettingStarted/features';
|
||||
import { CONNECTIONS_URL, CONNECT_URL, GETTING_STARTED_URL, PROVISIONING_URL } from '../constants';
|
||||
|
||||
export function getProvisioningRoutes(): RouteDescriptor[] {
|
||||
const featureToggles = config.featureToggles || {};
|
||||
if (!featureToggles.provisioning) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!checkRequiredFeatures()) {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -2,8 +2,20 @@ import { css } from '@emotion/css';
|
||||
import { useId, useState } from 'react';
|
||||
|
||||
import { createTheme, GrafanaTheme2, NewThemeOptions } from '@grafana/data';
|
||||
import { experimentalThemeDefinitions, NewThemeOptionsSchema } from '@grafana/data/internal';
|
||||
import { themeJsonSchema } from '@grafana/data/unstable';
|
||||
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 { t } from '@grafana/i18n';
|
||||
import { useChromeHeaderHeight } from '@grafana/runtime';
|
||||
import { CodeEditor, Combobox, Field, Stack, useStyles2 } from '@grafana/ui';
|
||||
@@ -34,8 +46,23 @@ 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(experimentalThemeDefinitions)) {
|
||||
for (const [name, json] of Object.entries(experimentalDefinitions)) {
|
||||
const result = NewThemeOptionsSchema.safeParse(json);
|
||||
if (!result.success) {
|
||||
console.error(`Invalid theme definition for theme ${name}: ${result.error.message}`);
|
||||
|
||||
@@ -85,6 +85,10 @@ export type GrafanaManagedReceiverConfig = {
|
||||
// SecureSettings?: GrafanaManagedReceiverConfigSettings<boolean>;
|
||||
settings: GrafanaManagedReceiverConfigSettings;
|
||||
type: string;
|
||||
/**
|
||||
* Version of the integration (e.g. "v0" for Mimir legacy, "v1" for Grafana)
|
||||
*/
|
||||
version?: string;
|
||||
/**
|
||||
* Name of the _receiver_, which in most cases will be the
|
||||
* same as the contact point's name. This should not be used, and is optional because the
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"type": "panel",
|
||||
"name": "Datagrid",
|
||||
"id": "datagrid",
|
||||
"state": "beta",
|
||||
"state": "deprecated",
|
||||
|
||||
"info": {
|
||||
"author": {
|
||||
|
||||
@@ -807,7 +807,8 @@
|
||||
"label-integration": "Integration",
|
||||
"label-notification-settings": "Notification settings",
|
||||
"label-section": "Optional {{name}} settings",
|
||||
"test": "Test"
|
||||
"test": "Test",
|
||||
"tooltip-legacy-version": "This is a legacy integration (version: {{version}}). It cannot be modified."
|
||||
},
|
||||
"classic-condition-viewer": {
|
||||
"of": "OF",
|
||||
@@ -2176,7 +2177,9 @@
|
||||
"provisioning": {
|
||||
"badge-tooltip-provenance": "This resource has been provisioned via {{provenance}} and cannot be edited through the UI",
|
||||
"badge-tooltip-standard": "This resource has been provisioned and cannot be edited through the UI",
|
||||
"body-imported": "This contact point contains integrations that were imported from an external Alertmanager and is currently read-only. The integrations will become editable after the migration process is complete.",
|
||||
"body-provisioned": "This {{resource}} has been provisioned, that means it was created by config. Please contact your server admin to update this {{resource}}.",
|
||||
"title-imported": "This contact point was imported and cannot be edited through the UI",
|
||||
"title-provisioned": "This {{resource}} cannot be edited through the UI"
|
||||
},
|
||||
"provisioning-badge": {
|
||||
|
||||
@@ -11,6 +11,7 @@ failed_checks=()
|
||||
for file in "$ARTIFACTS_DIR"/*.tgz; do
|
||||
echo "🔍 Checking NPM package: $file"
|
||||
|
||||
# If you need to debug ATTW issues, pass "--format json" to get verbose output.
|
||||
if ! NODE_OPTIONS="-C @grafana-app/source" yarn attw "$file" --ignore-rules "false-cjs" --profile "node16"; then
|
||||
echo "attw check failed for $file"
|
||||
echo ""
|
||||
|
||||
23
yarn.lock
23
yarn.lock
@@ -3324,6 +3324,7 @@ __metadata:
|
||||
react-use: "npm:17.6.0"
|
||||
rimraf: "npm:6.0.1"
|
||||
rollup: "npm:^4.22.4"
|
||||
rollup-plugin-copy: "npm:3.5.0"
|
||||
rollup-plugin-esbuild: "npm:6.2.1"
|
||||
rollup-plugin-node-externals: "npm:^8.0.0"
|
||||
rxjs: "npm:7.8.2"
|
||||
@@ -3789,11 +3790,11 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@grafana/scenes-react@npm:v6.52.1":
|
||||
version: 6.52.1
|
||||
resolution: "@grafana/scenes-react@npm:6.52.1"
|
||||
"@grafana/scenes-react@npm:6.52.2":
|
||||
version: 6.52.2
|
||||
resolution: "@grafana/scenes-react@npm:6.52.2"
|
||||
dependencies:
|
||||
"@grafana/scenes": "npm:6.52.1"
|
||||
"@grafana/scenes": "npm:6.52.2"
|
||||
lru-cache: "npm:^10.2.2"
|
||||
react-use: "npm:^17.4.0"
|
||||
peerDependencies:
|
||||
@@ -3805,7 +3806,7 @@ __metadata:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
react-router-dom: ^6.28.0
|
||||
checksum: 10/2f7c6ca8e26befd331808afb0cb934e2991e889a4de78be1122c536219676261c59c6204510761a1d4250fd44a3767818f0f225d23b2e7243cfc17baf8ca6ca3
|
||||
checksum: 10/c393faf6612e78254dab79b15cc970448d74ba9784ccda623953c5dbc21d91a8da94b7ad7d0d294eac51314cc193c419a7cb48295fd50b1f9c4472699669eb3e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3835,9 +3836,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@grafana/scenes@npm:6.52.1, @grafana/scenes@npm:v6.52.1":
|
||||
version: 6.52.1
|
||||
resolution: "@grafana/scenes@npm:6.52.1"
|
||||
"@grafana/scenes@npm:6.52.2":
|
||||
version: 6.52.2
|
||||
resolution: "@grafana/scenes@npm:6.52.2"
|
||||
dependencies:
|
||||
"@floating-ui/react": "npm:^0.26.16"
|
||||
"@leeoniya/ufuzzy": "npm:^1.0.16"
|
||||
@@ -3857,7 +3858,7 @@ __metadata:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
react-router-dom: ^6.28.0
|
||||
checksum: 10/d6172b51121e03c7dcbf30046772f99fc45922c1f7b360a7c3d2c0391300e378f306cb78251dda3b30895679379c38db30e4d52fee67a56cd95f18f38aadf3fb
|
||||
checksum: 10/f6dbe20db78bb1aa09cc38025534917887713d73119a172febb44700837ed859363ee0436b5f4bda6bc063f9432115e32519ab4c8da7834cf1fc22d43fea7711
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -19790,8 +19791,8 @@ __metadata:
|
||||
"@grafana/plugin-ui": "npm:^0.11.1"
|
||||
"@grafana/prometheus": "workspace:*"
|
||||
"@grafana/runtime": "workspace:*"
|
||||
"@grafana/scenes": "npm:v6.52.1"
|
||||
"@grafana/scenes-react": "npm:v6.52.1"
|
||||
"@grafana/scenes": "npm:6.52.2"
|
||||
"@grafana/scenes-react": "npm:6.52.2"
|
||||
"@grafana/schema": "workspace:*"
|
||||
"@grafana/sql": "workspace:*"
|
||||
"@grafana/test-utils": "workspace:*"
|
||||
|
||||
Reference in New Issue
Block a user