Compare commits
29 Commits
dual-write
...
pyroscope/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45e60f849b | ||
|
|
d43f4b576a | ||
|
|
f79f7ac33d | ||
|
|
f03ee8d19c | ||
|
|
eb37860388 | ||
|
|
da7b70336c | ||
|
|
17817bdda7 | ||
|
|
5bed426fd8 | ||
|
|
8bc405d5ed | ||
|
|
b8c9ee987e | ||
|
|
49e4d6760b | ||
|
|
665a54f02f | ||
|
|
ba6a783997 | ||
|
|
f704b8aa79 | ||
|
|
c1a46fdcb5 | ||
|
|
7143324229 | ||
|
|
48625d67e5 | ||
|
|
8bad33de4c | ||
|
|
040854c8af | ||
|
|
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
|
||||
|
||||
@@ -121,6 +121,8 @@ linters:
|
||||
- '**/pkg/tsdb/zipkin/**/*'
|
||||
- '**/pkg/tsdb/jaeger/*'
|
||||
- '**/pkg/tsdb/jaeger/**/*'
|
||||
- '**/pkg/tsdb/elasticsearch/*'
|
||||
- '**/pkg/tsdb/elasticsearch/**/*'
|
||||
deny:
|
||||
- pkg: github.com/grafana/grafana/pkg/api
|
||||
desc: Core plugins are not allowed to depend on Grafana core packages
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -103,10 +103,11 @@ To configure basic settings for the data source, complete the following steps:
|
||||
|
||||
1. Set the data source's basic configuration options:
|
||||
|
||||
| Name | Description |
|
||||
| ----------- | ------------------------------------------------------------------------ |
|
||||
| **Name** | Sets the name you use to refer to the data source in panels and queries. |
|
||||
| **Default** | Sets whether the data source is pre-selected for new panels. |
|
||||
| Name | Description |
|
||||
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | Sets the name you use to refer to the data source in panels and queries. |
|
||||
| **Default** | Sets whether the data source is pre-selected for new panels. |
|
||||
| **Universe Domain** | The universe domain to connect to. For more information, refer to [Documentation on universe domains](https://docs.cloud.google.com/python/docs/reference/monitoring/latest/google.cloud.monitoring_v3.services.service_monitoring_service.ServiceMonitoringServiceAsyncClient#google_cloud_monitoring_v3_services_service_monitoring_service_ServiceMonitoringServiceAsyncClient_universe_domain). Defaults to `googleapis.com`. |
|
||||
|
||||
### Provision the data source
|
||||
|
||||
@@ -129,6 +130,7 @@ datasources:
|
||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||
authenticationType: jwt
|
||||
defaultProject: my-project-name
|
||||
universeDomain: googleapis.com
|
||||
secureJsonData:
|
||||
privateKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
@@ -152,6 +154,7 @@ datasources:
|
||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||
authenticationType: jwt
|
||||
defaultProject: my-project-name
|
||||
universeDomain: googleapis.com
|
||||
privateKeyPath: /etc/secrets/gce.pem
|
||||
```
|
||||
|
||||
@@ -166,6 +169,7 @@ datasources:
|
||||
access: proxy
|
||||
jsonData:
|
||||
authenticationType: gce
|
||||
universeDomain: googleapis.com
|
||||
```
|
||||
|
||||
## Import pre-configured dashboards
|
||||
|
||||
@@ -87,6 +87,7 @@ With a Grafana Enterprise license, you also get access to premium data sources,
|
||||
- [CockroachDB](/grafana/plugins/grafana-cockroachdb-datasource)
|
||||
- [Databricks](/grafana/plugins/grafana-databricks-datasource)
|
||||
- [DataDog](/grafana/plugins/grafana-datadog-datasource)
|
||||
- [IBM Db2](/grafana/plugins/grafana-ibmdb2-datasource)
|
||||
- [Drone](/grafana/plugins/grafana-drone-datasource)
|
||||
- [DynamoDB](/grafana/plugins/grafana-dynamodb-datasource/)
|
||||
- [Dynatrace](/grafana/plugins/grafana-dynatrace-datasource)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -3743,46 +3743,21 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/aggregations.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/aggregations.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/configuration/DataLinks.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
|
||||
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=
|
||||
|
||||
@@ -82,6 +82,7 @@ module.exports = {
|
||||
// Decoupled plugins run their own tests so ignoring them here.
|
||||
'<rootDir>/public/app/plugins/datasource/azuremonitor',
|
||||
'<rootDir>/public/app/plugins/datasource/cloud-monitoring',
|
||||
'<rootDir>/public/app/plugins/datasource/elasticsearch',
|
||||
'<rootDir>/public/app/plugins/datasource/grafana-postgresql-datasource',
|
||||
'<rootDir>/public/app/plugins/datasource/grafana-pyroscope-datasource',
|
||||
'<rootDir>/public/app/plugins/datasource/grafana-testdata-datasource',
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"d3": "^7.8.5",
|
||||
"lodash": "4.17.21",
|
||||
"react": "18.3.1",
|
||||
"react-table": "^7.8.0",
|
||||
"react-use": "17.6.0",
|
||||
"react-virtualized-auto-sizer": "1.0.26",
|
||||
"tinycolor2": "1.6.0",
|
||||
@@ -81,6 +82,7 @@
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/react-virtualized-auto-sizer": "1.0.8",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"babel-jest": "29.7.0",
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { createDataFrame } from '@grafana/data';
|
||||
|
||||
import { FlameGraphDataContainer } from '../FlameGraph/dataTransform';
|
||||
import { data } from '../FlameGraph/testData/dataNestedSet';
|
||||
import { ColorScheme } from '../types';
|
||||
|
||||
import FlameGraphCallTreeContainer from './FlameGraphCallTreeContainer';
|
||||
|
||||
const meta: Meta<typeof FlameGraphCallTreeContainer> = {
|
||||
title: 'CallTree',
|
||||
component: FlameGraphCallTreeContainer,
|
||||
args: {
|
||||
colorScheme: ColorScheme.PackageBased,
|
||||
search: '',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ width: '100%', height: '1000px' }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Basic: StoryObj<typeof meta> = {
|
||||
render: (args) => {
|
||||
const dataContainer = new FlameGraphDataContainer(createDataFrame(data), { collapsing: true });
|
||||
|
||||
return (
|
||||
<FlameGraphCallTreeContainer
|
||||
{...args}
|
||||
data={dataContainer}
|
||||
onSymbolClick={(symbol) => {
|
||||
console.log('Symbol clicked:', symbol);
|
||||
}}
|
||||
onSandwich={(item) => {
|
||||
console.log('Sandwich:', item);
|
||||
}}
|
||||
onSearch={(symbol) => {
|
||||
console.log('Search:', symbol);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
580
packages/grafana-flamegraph/src/CallTree/utils.ts
Normal file
580
packages/grafana-flamegraph/src/CallTree/utils.ts
Normal file
@@ -0,0 +1,580 @@
|
||||
import { FlameGraphDataContainer, LevelItem } from '../FlameGraph/dataTransform';
|
||||
|
||||
export interface CallTreeNode {
|
||||
id: string; // Path-based ID (e.g., "0.2.1")
|
||||
label: string; // Function name
|
||||
self: number; // Self value
|
||||
total: number; // Total value
|
||||
selfPercent: number; // Self as % of root
|
||||
totalPercent: number; // Total as % of root
|
||||
depth: number; // Indentation level
|
||||
parentId?: string; // Parent node ID
|
||||
hasChildren: boolean; // Has expandable children
|
||||
childCount: number; // Number of direct children
|
||||
subtreeSize: number; // Total number of nodes in subtree (excluding self)
|
||||
levelItem: LevelItem; // Reference to original data
|
||||
subRows?: CallTreeNode[]; // Child nodes for react-table useExpanded
|
||||
isLastChild: boolean; // Whether this is the last child of its parent
|
||||
|
||||
// For diff profiles
|
||||
selfRight?: number;
|
||||
totalRight?: number;
|
||||
selfPercentRight?: number;
|
||||
totalPercentRight?: number;
|
||||
diffPercent?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build hierarchical call tree node from the LevelItem structure.
|
||||
* Each node gets a unique ID based on its path in the tree.
|
||||
* Children are stored in the subRows property for react-table useExpanded.
|
||||
*/
|
||||
export function buildCallTreeNode(
|
||||
data: FlameGraphDataContainer,
|
||||
rootItem: LevelItem,
|
||||
rootTotal: number,
|
||||
parentId?: string,
|
||||
parentDepth: number = -1,
|
||||
childIndex: number = 0
|
||||
): CallTreeNode {
|
||||
const nodeId = parentId ? `${parentId}.${childIndex}` : `${childIndex}`;
|
||||
const depth = parentDepth + 1;
|
||||
|
||||
// Get values for current item
|
||||
const itemIndex = rootItem.itemIndexes[0];
|
||||
const label = data.getLabel(itemIndex);
|
||||
const self = data.getSelf(itemIndex);
|
||||
const total = data.getValue(itemIndex);
|
||||
const selfPercent = rootTotal > 0 ? (self / rootTotal) * 100 : 0;
|
||||
const totalPercent = rootTotal > 0 ? (total / rootTotal) * 100 : 0;
|
||||
|
||||
// For diff profiles
|
||||
let selfRight: number | undefined;
|
||||
let totalRight: number | undefined;
|
||||
let selfPercentRight: number | undefined;
|
||||
let totalPercentRight: number | undefined;
|
||||
let diffPercent: number | undefined;
|
||||
|
||||
if (data.isDiffFlamegraph()) {
|
||||
selfRight = data.getSelfRight(itemIndex);
|
||||
totalRight = data.getValueRight(itemIndex);
|
||||
selfPercentRight = rootTotal > 0 ? (selfRight / rootTotal) * 100 : 0;
|
||||
totalPercentRight = rootTotal > 0 ? (totalRight / rootTotal) * 100 : 0;
|
||||
|
||||
// Calculate diff percentage (change from baseline to comparison)
|
||||
if (self > 0) {
|
||||
diffPercent = ((selfRight - self) / self) * 100;
|
||||
} else if (selfRight > 0) {
|
||||
diffPercent = Infinity; // New in comparison
|
||||
} else {
|
||||
diffPercent = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively build children
|
||||
const subRows =
|
||||
rootItem.children.length > 0
|
||||
? rootItem.children.map((child, index) => {
|
||||
const childNode = buildCallTreeNode(data, child, rootTotal, nodeId, depth, index);
|
||||
// Mark if this is the last child
|
||||
childNode.isLastChild = index === rootItem.children.length - 1;
|
||||
return childNode;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
// Calculate child count and subtree size
|
||||
const childCount = rootItem.children.length;
|
||||
const subtreeSize = subRows ? subRows.reduce((sum, child) => sum + child.subtreeSize + 1, 0) : 0;
|
||||
|
||||
const node: CallTreeNode = {
|
||||
id: nodeId,
|
||||
label,
|
||||
self,
|
||||
total,
|
||||
selfPercent,
|
||||
totalPercent,
|
||||
depth,
|
||||
parentId,
|
||||
hasChildren: rootItem.children.length > 0,
|
||||
childCount,
|
||||
subtreeSize,
|
||||
levelItem: rootItem,
|
||||
subRows,
|
||||
isLastChild: false, // Will be set by parent
|
||||
selfRight,
|
||||
totalRight,
|
||||
selfPercentRight,
|
||||
totalPercentRight,
|
||||
diffPercent,
|
||||
};
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build all call tree nodes from the root level items.
|
||||
* Returns an array of root nodes, each with their children in subRows.
|
||||
* This handles cases where there might be multiple root items.
|
||||
*/
|
||||
export function buildAllCallTreeNodes(data: FlameGraphDataContainer): CallTreeNode[] {
|
||||
const levels = data.getLevels();
|
||||
const rootTotal = levels.length > 0 ? levels[0][0].value : 0;
|
||||
|
||||
// Build hierarchical structure for each root item
|
||||
const rootNodes = levels[0].map((rootItem, index) => buildCallTreeNode(data, rootItem, rootTotal, undefined, -1, index));
|
||||
|
||||
return rootNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build call tree nodes from an array of levels (from mergeParentSubtrees).
|
||||
* This is used for the callers view where we get LevelItem[][] from getSandwichLevels.
|
||||
* Unlike buildCallTreeNode which recursively processes children, this function
|
||||
* processes pre-organized levels and builds the hierarchy from them.
|
||||
*/
|
||||
export function buildCallTreeFromLevels(
|
||||
levels: LevelItem[][],
|
||||
data: FlameGraphDataContainer,
|
||||
rootTotal: number
|
||||
): CallTreeNode[] {
|
||||
if (levels.length === 0 || levels[0].length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Map to track LevelItem -> CallTreeNode for building relationships
|
||||
const levelItemToNode = new Map<LevelItem, CallTreeNode>();
|
||||
|
||||
// Process each level and build nodes
|
||||
levels.forEach((level, levelIndex) => {
|
||||
level.forEach((levelItem, itemIndex) => {
|
||||
// Get values from data
|
||||
const itemDataIndex = levelItem.itemIndexes[0];
|
||||
const label = data.getLabel(itemDataIndex);
|
||||
const self = data.getSelf(itemDataIndex);
|
||||
const total = data.getValue(itemDataIndex);
|
||||
const selfPercent = rootTotal > 0 ? (self / rootTotal) * 100 : 0;
|
||||
const totalPercent = rootTotal > 0 ? (total / rootTotal) * 100 : 0;
|
||||
|
||||
// For diff profiles
|
||||
let selfRight: number | undefined;
|
||||
let totalRight: number | undefined;
|
||||
let selfPercentRight: number | undefined;
|
||||
let totalPercentRight: number | undefined;
|
||||
let diffPercent: number | undefined;
|
||||
|
||||
if (data.isDiffFlamegraph()) {
|
||||
selfRight = data.getSelfRight(itemDataIndex);
|
||||
totalRight = data.getValueRight(itemDataIndex);
|
||||
selfPercentRight = rootTotal > 0 ? (selfRight / rootTotal) * 100 : 0;
|
||||
totalPercentRight = rootTotal > 0 ? (totalRight / rootTotal) * 100 : 0;
|
||||
|
||||
// Calculate diff percentage
|
||||
if (self > 0) {
|
||||
diffPercent = ((selfRight - self) / self) * 100;
|
||||
} else if (selfRight > 0) {
|
||||
diffPercent = Infinity;
|
||||
} else {
|
||||
diffPercent = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine parent (if exists)
|
||||
let parentId: string | undefined;
|
||||
let depth = levelIndex;
|
||||
|
||||
if (levelItem.parents && levelItem.parents.length > 0) {
|
||||
const parentNode = levelItemToNode.get(levelItem.parents[0]);
|
||||
if (parentNode) {
|
||||
parentId = parentNode.id;
|
||||
depth = parentNode.depth + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate path-based ID
|
||||
// For root nodes, use index at level 0
|
||||
// For child nodes, append index to parent ID
|
||||
let nodeId: string;
|
||||
if (!parentId) {
|
||||
nodeId = `${itemIndex}`;
|
||||
} else {
|
||||
// Find index among siblings
|
||||
const parent = levelItemToNode.get(levelItem.parents![0]);
|
||||
const siblingIndex = parent?.subRows?.length || 0;
|
||||
nodeId = `${parentId}.${siblingIndex}`;
|
||||
}
|
||||
|
||||
// Create the node (without children initially)
|
||||
const node: CallTreeNode = {
|
||||
id: nodeId,
|
||||
label,
|
||||
self,
|
||||
total,
|
||||
selfPercent,
|
||||
totalPercent,
|
||||
depth,
|
||||
parentId,
|
||||
hasChildren: levelItem.children.length > 0,
|
||||
childCount: levelItem.children.length,
|
||||
subtreeSize: 0, // Will be calculated later
|
||||
levelItem,
|
||||
subRows: undefined,
|
||||
isLastChild: false,
|
||||
selfRight,
|
||||
totalRight,
|
||||
selfPercentRight,
|
||||
totalPercentRight,
|
||||
diffPercent,
|
||||
};
|
||||
|
||||
// Add to map
|
||||
levelItemToNode.set(levelItem, node);
|
||||
|
||||
// Add as child to parent
|
||||
if (levelItem.parents && levelItem.parents.length > 0) {
|
||||
const parentNode = levelItemToNode.get(levelItem.parents[0]);
|
||||
if (parentNode) {
|
||||
if (!parentNode.subRows) {
|
||||
parentNode.subRows = [];
|
||||
}
|
||||
parentNode.subRows.push(node);
|
||||
// Mark if this is the last child
|
||||
const isLastChild = parentNode.subRows.length === parentNode.childCount;
|
||||
node.isLastChild = isLastChild;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Calculate subtreeSize for all nodes (bottom-up)
|
||||
const calculateSubtreeSize = (node: CallTreeNode): number => {
|
||||
if (!node.subRows || node.subRows.length === 0) {
|
||||
node.subtreeSize = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
const size = node.subRows.reduce((sum, child) => {
|
||||
return sum + calculateSubtreeSize(child) + 1;
|
||||
}, 0);
|
||||
|
||||
node.subtreeSize = size;
|
||||
return size;
|
||||
};
|
||||
|
||||
// Collect root nodes (level 0)
|
||||
const rootNodes: CallTreeNode[] = [];
|
||||
levels[0].forEach((levelItem) => {
|
||||
const node = levelItemToNode.get(levelItem);
|
||||
if (node) {
|
||||
calculateSubtreeSize(node);
|
||||
rootNodes.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
return rootNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively collect expanded state for nodes up to a certain depth.
|
||||
*/
|
||||
function collectExpandedByDepth(
|
||||
node: CallTreeNode,
|
||||
levelsToExpand: number,
|
||||
expanded: Record<string, boolean>
|
||||
): void {
|
||||
if (node.depth < levelsToExpand && node.hasChildren) {
|
||||
expanded[node.id] = true;
|
||||
}
|
||||
|
||||
if (node.subRows) {
|
||||
node.subRows.forEach((child) => collectExpandedByDepth(child, levelsToExpand, expanded));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get initial expanded state for the tree.
|
||||
* Auto-expands first N levels.
|
||||
*/
|
||||
export function getInitialExpandedState(nodes: CallTreeNode[], levelsToExpand: number = 2): Record<string, boolean> {
|
||||
const expanded: Record<string, boolean> = {};
|
||||
|
||||
nodes.forEach((node) => {
|
||||
collectExpandedByDepth(node, levelsToExpand, expanded);
|
||||
});
|
||||
|
||||
return expanded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restructure the callers tree to show a specific target node at the root.
|
||||
* In the callers view, we want to show the target function with its callers as children.
|
||||
* This function finds the target node and collects all paths that lead to it,
|
||||
* then restructures them so the target is at the root.
|
||||
*/
|
||||
export function restructureCallersTree(
|
||||
nodes: CallTreeNode[],
|
||||
targetLabel: string
|
||||
): { restructuredTree: CallTreeNode[]; targetNode: CallTreeNode | undefined } {
|
||||
// First, find all paths from root to target node
|
||||
const findPathsToTarget = (
|
||||
nodes: CallTreeNode[],
|
||||
targetLabel: string,
|
||||
currentPath: CallTreeNode[] = []
|
||||
): CallTreeNode[][] => {
|
||||
const paths: CallTreeNode[][] = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
const newPath = [...currentPath, node];
|
||||
|
||||
if (node.label === targetLabel) {
|
||||
// Found a path to the target
|
||||
paths.push(newPath);
|
||||
}
|
||||
|
||||
if (node.subRows && node.subRows.length > 0) {
|
||||
// Continue searching in children
|
||||
const childPaths = findPathsToTarget(node.subRows, targetLabel, newPath);
|
||||
paths.push(...childPaths);
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
};
|
||||
|
||||
const paths = findPathsToTarget(nodes, targetLabel);
|
||||
|
||||
if (paths.length === 0) {
|
||||
// Target not found, return original tree
|
||||
return { restructuredTree: nodes, targetNode: undefined };
|
||||
}
|
||||
|
||||
// Get the target node from the first path (they should all have the same target node)
|
||||
const targetNode = paths[0][paths[0].length - 1];
|
||||
|
||||
// Now restructure: create a new tree with target at root
|
||||
// Each path to the target becomes a branch under the target
|
||||
// For example, if we have: root -> A -> B -> target
|
||||
// We want: target -> B -> A -> root (inverted)
|
||||
|
||||
const buildInvertedChildren = (paths: CallTreeNode[][]): CallTreeNode[] => {
|
||||
// Group paths by their immediate caller (the node right before target)
|
||||
const callerGroups = new Map<string, CallTreeNode[][]>();
|
||||
|
||||
for (const path of paths) {
|
||||
if (path.length <= 1) {
|
||||
// Path is just the target node itself, no callers
|
||||
continue;
|
||||
}
|
||||
|
||||
// The immediate caller is the node right before the target
|
||||
const immediateCaller = path[path.length - 2];
|
||||
const callerKey = immediateCaller.label;
|
||||
|
||||
if (!callerGroups.has(callerKey)) {
|
||||
callerGroups.set(callerKey, []);
|
||||
}
|
||||
callerGroups.get(callerKey)!.push(path);
|
||||
}
|
||||
|
||||
// Build nodes for each immediate caller
|
||||
const callerNodes: CallTreeNode[] = [];
|
||||
let callerIndex = 0;
|
||||
|
||||
for (const [, callerPaths] of callerGroups.entries()) {
|
||||
// Get the immediate caller node from one of the paths
|
||||
const immediateCallerNode = callerPaths[0][callerPaths[0].length - 2];
|
||||
|
||||
// For this caller, recursively build its callers (from the remaining path)
|
||||
const remainingPaths = callerPaths.map((path) => path.slice(0, -1)); // Remove target from paths
|
||||
const grandCallers = buildInvertedChildren(remainingPaths);
|
||||
|
||||
// Create a new node for this caller as a child of the target
|
||||
const newCallerId = `0.${callerIndex}`;
|
||||
const callerNode: CallTreeNode = {
|
||||
...immediateCallerNode,
|
||||
id: newCallerId,
|
||||
depth: 1,
|
||||
parentId: '0',
|
||||
subRows: grandCallers.length > 0 ? grandCallers : undefined,
|
||||
hasChildren: grandCallers.length > 0,
|
||||
childCount: grandCallers.length,
|
||||
isLastChild: callerIndex === callerGroups.size - 1,
|
||||
};
|
||||
|
||||
// Update IDs of grandCallers
|
||||
if (grandCallers.length > 0) {
|
||||
grandCallers.forEach((grandCaller, idx) => {
|
||||
updateNodeIds(grandCaller, newCallerId, idx);
|
||||
});
|
||||
}
|
||||
|
||||
callerNodes.push(callerNode);
|
||||
callerIndex++;
|
||||
}
|
||||
|
||||
return callerNodes;
|
||||
};
|
||||
|
||||
// Helper to recursively update node IDs
|
||||
const updateNodeIds = (node: CallTreeNode, parentId: string, index: number) => {
|
||||
node.id = `${parentId}.${index}`;
|
||||
node.parentId = parentId;
|
||||
node.depth = parentId.split('.').length;
|
||||
|
||||
if (node.subRows) {
|
||||
node.subRows.forEach((child, idx) => {
|
||||
updateNodeIds(child, node.id, idx);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Build the inverted children for the target
|
||||
const invertedChildren = buildInvertedChildren(paths);
|
||||
|
||||
// Create the restructured target node as root
|
||||
const restructuredTarget: CallTreeNode = {
|
||||
...targetNode,
|
||||
id: '0',
|
||||
depth: 0,
|
||||
parentId: undefined,
|
||||
subRows: invertedChildren.length > 0 ? invertedChildren : undefined,
|
||||
hasChildren: invertedChildren.length > 0,
|
||||
childCount: invertedChildren.length,
|
||||
subtreeSize: invertedChildren.reduce((sum, child) => sum + child.subtreeSize + 1, 0),
|
||||
isLastChild: false,
|
||||
};
|
||||
|
||||
return { restructuredTree: [restructuredTarget], targetNode: restructuredTarget };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a callers tree directly from sandwich levels data.
|
||||
* This creates an inverted tree where the target function is at the root
|
||||
* and its callers are shown as children.
|
||||
*/
|
||||
export function buildCallersTreeFromLevels(
|
||||
levels: LevelItem[][],
|
||||
targetLabel: string,
|
||||
data: FlameGraphDataContainer,
|
||||
rootTotal: number
|
||||
): { tree: CallTreeNode[]; targetNode: CallTreeNode | undefined } {
|
||||
if (levels.length === 0) {
|
||||
return { tree: [], targetNode: undefined };
|
||||
}
|
||||
|
||||
// Find the target node in the levels
|
||||
let targetLevelIndex = -1;
|
||||
let targetItem: LevelItem | undefined;
|
||||
|
||||
for (let i = 0; i < levels.length; i++) {
|
||||
for (const item of levels[i]) {
|
||||
const label = data.getLabel(item.itemIndexes[0]);
|
||||
if (label === targetLabel) {
|
||||
targetLevelIndex = i;
|
||||
targetItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (targetItem) break;
|
||||
}
|
||||
|
||||
if (!targetItem || targetLevelIndex === -1) {
|
||||
// Target not found
|
||||
return { tree: [], targetNode: undefined };
|
||||
}
|
||||
|
||||
// Create a map from LevelItem to all items that reference it as a parent
|
||||
const childrenMap = new Map<LevelItem, LevelItem[]>();
|
||||
|
||||
for (const level of levels) {
|
||||
for (const item of level) {
|
||||
if (item.parents) {
|
||||
for (const parent of item.parents) {
|
||||
if (!childrenMap.has(parent)) {
|
||||
childrenMap.set(parent, []);
|
||||
}
|
||||
childrenMap.get(parent)!.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the inverted tree recursively
|
||||
// For callers view: the target is root, and parents become children
|
||||
const buildInvertedNode = (
|
||||
item: LevelItem,
|
||||
nodeId: string,
|
||||
depth: number,
|
||||
parentId: string | undefined
|
||||
): CallTreeNode => {
|
||||
const itemIdx = item.itemIndexes[0];
|
||||
const label = data.getLabel(itemIdx);
|
||||
const self = data.getSelf(itemIdx);
|
||||
const total = data.getValue(itemIdx);
|
||||
const selfPercent = rootTotal > 0 ? (self / rootTotal) * 100 : 0;
|
||||
const totalPercent = rootTotal > 0 ? (total / rootTotal) * 100 : 0;
|
||||
|
||||
// For diff profiles
|
||||
let selfRight: number | undefined;
|
||||
let totalRight: number | undefined;
|
||||
let selfPercentRight: number | undefined;
|
||||
let totalPercentRight: number | undefined;
|
||||
let diffPercent: number | undefined;
|
||||
|
||||
if (data.isDiffFlamegraph()) {
|
||||
selfRight = data.getSelfRight(itemIdx);
|
||||
totalRight = data.getValueRight(itemIdx);
|
||||
selfPercentRight = rootTotal > 0 ? (selfRight / rootTotal) * 100 : 0;
|
||||
totalPercentRight = rootTotal > 0 ? (totalRight / rootTotal) * 100 : 0;
|
||||
|
||||
if (self > 0) {
|
||||
diffPercent = ((selfRight - self) / self) * 100;
|
||||
} else if (selfRight > 0) {
|
||||
diffPercent = Infinity;
|
||||
} else {
|
||||
diffPercent = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// In the inverted tree, parents become children (callers)
|
||||
const callers = item.parents || [];
|
||||
const subRows =
|
||||
callers.length > 0
|
||||
? callers.map((caller, idx) => {
|
||||
const callerId = `${nodeId}.${idx}`;
|
||||
const callerNode = buildInvertedNode(caller, callerId, depth + 1, nodeId);
|
||||
callerNode.isLastChild = idx === callers.length - 1;
|
||||
return callerNode;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const childCount = callers.length;
|
||||
const subtreeSize = subRows ? subRows.reduce((sum, child) => sum + child.subtreeSize + 1, 0) : 0;
|
||||
|
||||
return {
|
||||
id: nodeId,
|
||||
label,
|
||||
self,
|
||||
total,
|
||||
selfPercent,
|
||||
totalPercent,
|
||||
depth,
|
||||
parentId,
|
||||
hasChildren: callers.length > 0,
|
||||
childCount,
|
||||
subtreeSize,
|
||||
levelItem: item,
|
||||
subRows,
|
||||
isLastChild: false,
|
||||
selfRight,
|
||||
totalRight,
|
||||
selfPercentRight,
|
||||
totalPercentRight,
|
||||
diffPercent,
|
||||
};
|
||||
};
|
||||
|
||||
// Build tree with target as root
|
||||
const targetNode = buildInvertedNode(targetItem, '0', 0, undefined);
|
||||
|
||||
return { tree: [targetNode], targetNode };
|
||||
}
|
||||
@@ -16,7 +16,7 @@ const meta: Meta<typeof FlameGraph> = {
|
||||
rangeMax: 1,
|
||||
textAlign: 'left',
|
||||
colorScheme: ColorScheme.PackageBased,
|
||||
selectedView: SelectedView.Both,
|
||||
selectedView: SelectedView.Multi,
|
||||
search: '',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -43,10 +43,13 @@ describe('FlameGraph', () => {
|
||||
setRangeMax={setRangeMax}
|
||||
onItemFocused={onItemFocused}
|
||||
textAlign={'left'}
|
||||
onTextAlignChange={jest.fn()}
|
||||
onSandwich={onSandwich}
|
||||
onFocusPillClick={onFocusPillClick}
|
||||
onSandwichPillClick={onSandwichPillClick}
|
||||
colorScheme={ColorScheme.ValueBased}
|
||||
onColorSchemeChange={jest.fn()}
|
||||
isDiffMode={false}
|
||||
selectedView={SelectedView.FlameGraph}
|
||||
search={''}
|
||||
collapsedMap={container.getCollapsedMap()}
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Icon } from '@grafana/ui';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Button, ButtonGroup, Dropdown, Icon, Menu, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { byPackageGradient, byValueGradient, diffColorBlindGradient, diffDefaultGradient } from './colors';
|
||||
import { PIXELS_PER_LEVEL } from '../constants';
|
||||
import { ClickedItemData, ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from '../types';
|
||||
|
||||
@@ -39,11 +41,14 @@ type Props = {
|
||||
onItemFocused: (data: ClickedItemData) => void;
|
||||
focusedItemData?: ClickedItemData;
|
||||
textAlign: TextAlign;
|
||||
onTextAlignChange: (align: TextAlign) => void;
|
||||
sandwichItem?: string;
|
||||
onSandwich: (label: string) => void;
|
||||
onFocusPillClick: () => void;
|
||||
onSandwichPillClick: () => void;
|
||||
colorScheme: ColorScheme | ColorSchemeDiff;
|
||||
onColorSchemeChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
|
||||
isDiffMode: boolean;
|
||||
showFlameGraphOnly?: boolean;
|
||||
getExtraContextMenuButtons?: GetExtraContextMenuButtonsFunction;
|
||||
collapsing?: boolean;
|
||||
@@ -63,11 +68,14 @@ const FlameGraph = ({
|
||||
onItemFocused,
|
||||
focusedItemData,
|
||||
textAlign,
|
||||
onTextAlignChange,
|
||||
onSandwich,
|
||||
sandwichItem,
|
||||
onFocusPillClick,
|
||||
onSandwichPillClick,
|
||||
colorScheme,
|
||||
onColorSchemeChange,
|
||||
isDiffMode,
|
||||
showFlameGraphOnly,
|
||||
getExtraContextMenuButtons,
|
||||
collapsing,
|
||||
@@ -76,7 +84,7 @@ const FlameGraph = ({
|
||||
collapsedMap,
|
||||
setCollapsedMap,
|
||||
}: Props) => {
|
||||
const styles = getStyles();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const [levels, setLevels] = useState<LevelItem[][]>();
|
||||
const [levelsCallers, setLevelsCallers] = useState<LevelItem[][]>();
|
||||
@@ -175,28 +183,183 @@ const FlameGraph = ({
|
||||
);
|
||||
}
|
||||
|
||||
const alignOptions: Array<SelectableValue<TextAlign>> = [
|
||||
{ value: 'left', description: 'Align text left', icon: 'align-left' },
|
||||
{ value: 'right', description: 'Align text right', icon: 'align-right' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.graph}>
|
||||
<FlameGraphMetadata
|
||||
data={data}
|
||||
focusedItem={focusedItemData}
|
||||
sandwichedLabel={sandwichItem}
|
||||
totalTicks={totalViewTicks}
|
||||
onFocusPillClick={onFocusPillClick}
|
||||
onSandwichPillClick={onSandwichPillClick}
|
||||
/>
|
||||
<div className={styles.toolbar}>
|
||||
<FlameGraphMetadata
|
||||
data={data}
|
||||
focusedItem={focusedItemData}
|
||||
sandwichedLabel={sandwichItem}
|
||||
totalTicks={totalViewTicks}
|
||||
onFocusPillClick={onFocusPillClick}
|
||||
onSandwichPillClick={onSandwichPillClick}
|
||||
/>
|
||||
<div className={styles.controls}>
|
||||
<ColorSchemeButton value={colorScheme} onChange={onColorSchemeChange} isDiffMode={isDiffMode} />
|
||||
<ButtonGroup className={styles.buttonSpacing}>
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
fill={'outline'}
|
||||
size={'sm'}
|
||||
tooltip={'Expand all groups'}
|
||||
onClick={() => {
|
||||
setCollapsedMap(collapsedMap.setAllCollapsedStatus(false));
|
||||
}}
|
||||
aria-label={'Expand all groups'}
|
||||
icon={'angle-double-down'}
|
||||
/>
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
fill={'outline'}
|
||||
size={'sm'}
|
||||
tooltip={'Collapse all groups'}
|
||||
onClick={() => {
|
||||
setCollapsedMap(collapsedMap.setAllCollapsedStatus(true));
|
||||
}}
|
||||
aria-label={'Collapse all groups'}
|
||||
icon={'angle-double-up'}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<RadioButtonGroup<TextAlign>
|
||||
size="sm"
|
||||
options={alignOptions}
|
||||
value={textAlign}
|
||||
onChange={onTextAlignChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{canvas}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = () => ({
|
||||
type ColorSchemeButtonProps = {
|
||||
value: ColorScheme | ColorSchemeDiff;
|
||||
onChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
|
||||
isDiffMode: boolean;
|
||||
};
|
||||
|
||||
function ColorSchemeButton(props: ColorSchemeButtonProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
let menu = (
|
||||
<Menu>
|
||||
<Menu.Item label="By package name" onClick={() => props.onChange(ColorScheme.PackageBased)} />
|
||||
<Menu.Item label="By value" onClick={() => props.onChange(ColorScheme.ValueBased)} />
|
||||
</Menu>
|
||||
);
|
||||
|
||||
// Show a bit different gradient as a way to indicate selected value
|
||||
const colorDotStyle =
|
||||
{
|
||||
[ColorScheme.ValueBased]: styles.colorDotByValue,
|
||||
[ColorScheme.PackageBased]: styles.colorDotByPackage,
|
||||
[ColorSchemeDiff.DiffColorBlind]: styles.colorDotDiffColorBlind,
|
||||
[ColorSchemeDiff.Default]: styles.colorDotDiffDefault,
|
||||
}[props.value] || styles.colorDotByValue;
|
||||
|
||||
let contents = <span className={cx(styles.colorDot, colorDotStyle)} />;
|
||||
|
||||
if (props.isDiffMode) {
|
||||
menu = (
|
||||
<Menu>
|
||||
<Menu.Item label="Default (green to red)" onClick={() => props.onChange(ColorSchemeDiff.Default)} />
|
||||
<Menu.Item label="Color blind (blue to red)" onClick={() => props.onChange(ColorSchemeDiff.DiffColorBlind)} />
|
||||
</Menu>
|
||||
);
|
||||
|
||||
contents = (
|
||||
<div className={cx(styles.colorDotDiff, colorDotStyle)}>
|
||||
<div>-100% (removed)</div>
|
||||
<div>0%</div>
|
||||
<div>+100% (added)</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown overlay={menu}>
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
fill={'outline'}
|
||||
size={'sm'}
|
||||
tooltip={'Change color scheme'}
|
||||
onClick={() => {}}
|
||||
className={styles.buttonSpacing}
|
||||
aria-label={'Change color scheme'}
|
||||
>
|
||||
{contents}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
graph: css({
|
||||
label: 'graph',
|
||||
overflow: 'auto',
|
||||
flexGrow: 1,
|
||||
flexBasis: '50%',
|
||||
}),
|
||||
toolbar: css({
|
||||
label: 'toolbar',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.spacing(1),
|
||||
}),
|
||||
controls: css({
|
||||
label: 'controls',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
buttonSpacing: css({
|
||||
label: 'buttonSpacing',
|
||||
marginRight: theme.spacing(1),
|
||||
}),
|
||||
colorDot: css({
|
||||
label: 'colorDot',
|
||||
display: 'inline-block',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
borderRadius: theme.shape.radius.circle,
|
||||
}),
|
||||
colorDotDiff: css({
|
||||
label: 'colorDotDiff',
|
||||
display: 'flex',
|
||||
width: '200px',
|
||||
height: '12px',
|
||||
color: 'white',
|
||||
fontSize: 9,
|
||||
lineHeight: 1.3,
|
||||
fontWeight: 300,
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 2px',
|
||||
// We have a specific sizing for this so probably makes sense to use hardcoded value here
|
||||
// eslint-disable-next-line @grafana/no-border-radius-literal
|
||||
borderRadius: '2px',
|
||||
}),
|
||||
colorDotByValue: css({
|
||||
label: 'colorDotByValue',
|
||||
background: byValueGradient,
|
||||
}),
|
||||
colorDotByPackage: css({
|
||||
label: 'colorDotByPackage',
|
||||
background: byPackageGradient,
|
||||
}),
|
||||
colorDotDiffDefault: css({
|
||||
label: 'colorDotDiffDefault',
|
||||
background: diffDefaultGradient,
|
||||
}),
|
||||
colorDotDiffColorBlind: css({
|
||||
label: 'colorDotDiffColorBlind',
|
||||
background: diffColorBlindGradient,
|
||||
}),
|
||||
sandwichCanvasWrapper: css({
|
||||
label: 'sandwichCanvasWrapper',
|
||||
display: 'flex',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,18 @@
|
||||
import { css } from '@emotion/css';
|
||||
import uFuzzy from '@leeoniya/ufuzzy';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
|
||||
import { DataFrame, GrafanaTheme2, escapeStringForRegex } from '@grafana/data';
|
||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
||||
import { ThemeContext } from '@grafana/ui';
|
||||
|
||||
import FlameGraph from './FlameGraph/FlameGraph';
|
||||
import { FlameGraphDataContainer } from './FlameGraph/dataTransform';
|
||||
import { GetExtraContextMenuButtonsFunction } from './FlameGraph/FlameGraphContextMenu';
|
||||
import { CollapsedMap, FlameGraphDataContainer } from './FlameGraph/dataTransform';
|
||||
import FlameGraphHeader from './FlameGraphHeader';
|
||||
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
|
||||
import FlameGraphPane from './FlameGraphPane';
|
||||
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from './constants';
|
||||
import { ClickedItemData, ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
|
||||
import { PaneView, SelectedView, ViewMode } from './types';
|
||||
import { getAssistantContextFromDataFrame } from './utils';
|
||||
|
||||
const ufuzzy = new uFuzzy();
|
||||
@@ -104,17 +103,18 @@ const FlameGraphContainer = ({
|
||||
getExtraContextMenuButtons,
|
||||
showAnalyzeWithAssistant = true,
|
||||
}: Props) => {
|
||||
const [focusedItemData, setFocusedItemData] = useState<ClickedItemData>();
|
||||
|
||||
const [rangeMin, setRangeMin] = useState(0);
|
||||
const [rangeMax, setRangeMax] = useState(1);
|
||||
// Shared state across all views
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedView, setSelectedView] = useState(SelectedView.Both);
|
||||
const [selectedView, setSelectedView] = useState(SelectedView.Multi);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>(ViewMode.Split);
|
||||
const [leftPaneView, setLeftPaneView] = useState<PaneView>(PaneView.TopTable);
|
||||
const [rightPaneView, setRightPaneView] = useState<PaneView>(PaneView.FlameGraph);
|
||||
const [singleView, setSingleView] = useState<PaneView>(PaneView.FlameGraph);
|
||||
const [sizeRef, { width: containerWidth }] = useMeasure<HTMLDivElement>();
|
||||
const [textAlign, setTextAlign] = useState<TextAlign>('left');
|
||||
// This is a label of the item because in sandwich view we group all items by label and present a merged graph
|
||||
const [sandwichItem, setSandwichItem] = useState<string>();
|
||||
const [collapsedMap, setCollapsedMap] = useState(new CollapsedMap());
|
||||
// Used to trigger reset of pane-specific state (focus, sandwich) when parent reset button is clicked
|
||||
const [resetKey, setResetKey] = useState(0);
|
||||
// Track if we temporarily switched away from Both view due to narrow width
|
||||
const [viewBeforeNarrow, setViewBeforeNarrow] = useState<SelectedView | null>(null);
|
||||
|
||||
const theme = useMemo(() => getTheme(), [getTheme]);
|
||||
const dataContainer = useMemo((): FlameGraphDataContainer | undefined => {
|
||||
@@ -122,157 +122,220 @@ const FlameGraphContainer = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const container = new FlameGraphDataContainer(data, { collapsing: !disableCollapsing }, theme);
|
||||
setCollapsedMap(container.getCollapsedMap());
|
||||
return container;
|
||||
return new FlameGraphDataContainer(data, { collapsing: !disableCollapsing }, theme);
|
||||
}, [data, theme, disableCollapsing]);
|
||||
const [colorScheme, setColorScheme] = useColorScheme(dataContainer);
|
||||
|
||||
const styles = getStyles(theme);
|
||||
const matchedLabels = useLabelSearch(search, dataContainer);
|
||||
|
||||
// If user resizes window with both as the selected view
|
||||
// Handle responsive layout: switch away from Both view when narrow, restore when wide again
|
||||
useEffect(() => {
|
||||
if (
|
||||
containerWidth > 0 &&
|
||||
containerWidth < MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH &&
|
||||
selectedView === SelectedView.Both &&
|
||||
!vertical
|
||||
) {
|
||||
setSelectedView(SelectedView.FlameGraph);
|
||||
}
|
||||
}, [selectedView, setSelectedView, containerWidth, vertical]);
|
||||
|
||||
const resetFocus = useCallback(() => {
|
||||
setFocusedItemData(undefined);
|
||||
setRangeMin(0);
|
||||
setRangeMax(1);
|
||||
}, [setFocusedItemData, setRangeMax, setRangeMin]);
|
||||
|
||||
const resetSandwich = useCallback(() => {
|
||||
setSandwichItem(undefined);
|
||||
}, [setSandwichItem]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!keepFocusOnDataChange) {
|
||||
resetFocus();
|
||||
resetSandwich();
|
||||
if (containerWidth === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataContainer && focusedItemData) {
|
||||
const item = dataContainer.getNodesWithLabel(focusedItemData.label)?.[0];
|
||||
const isNarrow = containerWidth < MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH && !vertical;
|
||||
|
||||
if (item) {
|
||||
setFocusedItemData({ ...focusedItemData, item });
|
||||
|
||||
const levels = dataContainer.getLevels();
|
||||
const totalViewTicks = levels.length ? levels[0][0].value : 0;
|
||||
setRangeMin(item.start / totalViewTicks);
|
||||
setRangeMax((item.start + item.value) / totalViewTicks);
|
||||
} else {
|
||||
setFocusedItemData({
|
||||
...focusedItemData,
|
||||
item: {
|
||||
start: 0,
|
||||
value: 0,
|
||||
itemIndexes: [],
|
||||
children: [],
|
||||
level: 0,
|
||||
},
|
||||
});
|
||||
|
||||
setRangeMin(0);
|
||||
setRangeMax(1);
|
||||
}
|
||||
if (isNarrow && selectedView === SelectedView.Multi) {
|
||||
// Going narrow: save current view and switch to FlameGraph
|
||||
setViewBeforeNarrow(SelectedView.Multi);
|
||||
setSelectedView(SelectedView.FlameGraph);
|
||||
} else if (!isNarrow && viewBeforeNarrow !== null) {
|
||||
// Going wide again: restore the previous view
|
||||
setSelectedView(viewBeforeNarrow);
|
||||
setViewBeforeNarrow(null);
|
||||
}
|
||||
}, [dataContainer, keepFocusOnDataChange]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const onSymbolClick = useCallback(
|
||||
(symbol: string) => {
|
||||
const anchored = `^${escapeStringForRegex(symbol)}$`;
|
||||
|
||||
if (search === anchored) {
|
||||
setSearch('');
|
||||
} else {
|
||||
onTableSymbolClick?.(symbol);
|
||||
setSearch(anchored);
|
||||
resetFocus();
|
||||
}
|
||||
},
|
||||
[setSearch, resetFocus, onTableSymbolClick, search]
|
||||
);
|
||||
}, [containerWidth, vertical, selectedView, viewBeforeNarrow]);
|
||||
|
||||
if (!dataContainer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const flameGraph = (
|
||||
<FlameGraph
|
||||
data={dataContainer}
|
||||
rangeMin={rangeMin}
|
||||
rangeMax={rangeMax}
|
||||
matchedLabels={matchedLabels}
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
onItemFocused={(data) => setFocusedItemData(data)}
|
||||
focusedItemData={focusedItemData}
|
||||
textAlign={textAlign}
|
||||
sandwichItem={sandwichItem}
|
||||
onSandwich={(label: string) => {
|
||||
resetFocus();
|
||||
setSandwichItem(label);
|
||||
}}
|
||||
onFocusPillClick={resetFocus}
|
||||
onSandwichPillClick={resetSandwich}
|
||||
colorScheme={colorScheme}
|
||||
showFlameGraphOnly={showFlameGraphOnly}
|
||||
collapsing={!disableCollapsing}
|
||||
getExtraContextMenuButtons={getExtraContextMenuButtons}
|
||||
selectedView={selectedView}
|
||||
search={search}
|
||||
collapsedMap={collapsedMap}
|
||||
setCollapsedMap={setCollapsedMap}
|
||||
/>
|
||||
);
|
||||
|
||||
const table = (
|
||||
<FlameGraphTopTableContainer
|
||||
data={dataContainer}
|
||||
onSymbolClick={onSymbolClick}
|
||||
search={search}
|
||||
matchedLabels={matchedLabels}
|
||||
sandwichItem={sandwichItem}
|
||||
onSandwich={setSandwichItem}
|
||||
onSearch={(str) => {
|
||||
if (!str) {
|
||||
setSearch('');
|
||||
return;
|
||||
}
|
||||
setSearch(`^${escapeStringForRegex(str)}$`);
|
||||
}}
|
||||
onTableSort={onTableSort}
|
||||
colorScheme={colorScheme}
|
||||
/>
|
||||
);
|
||||
|
||||
let body;
|
||||
if (showFlameGraphOnly || selectedView === SelectedView.FlameGraph) {
|
||||
body = flameGraph;
|
||||
body = (
|
||||
<FlameGraphPane
|
||||
paneView={PaneView.FlameGraph}
|
||||
dataContainer={dataContainer}
|
||||
search={search}
|
||||
matchedLabels={matchedLabels}
|
||||
onTableSymbolClick={onTableSymbolClick}
|
||||
onTextAlignSelected={onTextAlignSelected}
|
||||
onTableSort={onTableSort}
|
||||
showFlameGraphOnly={showFlameGraphOnly}
|
||||
disableCollapsing={disableCollapsing}
|
||||
getExtraContextMenuButtons={getExtraContextMenuButtons}
|
||||
selectedView={selectedView}
|
||||
viewMode={viewMode}
|
||||
theme={theme}
|
||||
setSearch={setSearch}
|
||||
resetKey={resetKey}
|
||||
keepFocusOnDataChange={keepFocusOnDataChange}
|
||||
/>
|
||||
);
|
||||
} else if (selectedView === SelectedView.TopTable) {
|
||||
body = <div className={styles.tableContainer}>{table}</div>;
|
||||
} else if (selectedView === SelectedView.Both) {
|
||||
if (vertical) {
|
||||
body = (
|
||||
<div>
|
||||
<div className={styles.verticalGraphContainer}>{flameGraph}</div>
|
||||
<div className={styles.verticalTableContainer}>{table}</div>
|
||||
</div>
|
||||
);
|
||||
body = (
|
||||
<FlameGraphPane
|
||||
paneView={PaneView.TopTable}
|
||||
dataContainer={dataContainer}
|
||||
search={search}
|
||||
matchedLabels={matchedLabels}
|
||||
onTableSymbolClick={onTableSymbolClick}
|
||||
onTextAlignSelected={onTextAlignSelected}
|
||||
onTableSort={onTableSort}
|
||||
showFlameGraphOnly={showFlameGraphOnly}
|
||||
disableCollapsing={disableCollapsing}
|
||||
getExtraContextMenuButtons={getExtraContextMenuButtons}
|
||||
selectedView={selectedView}
|
||||
viewMode={viewMode}
|
||||
theme={theme}
|
||||
setSearch={setSearch}
|
||||
resetKey={resetKey}
|
||||
keepFocusOnDataChange={keepFocusOnDataChange}
|
||||
/>
|
||||
);
|
||||
} else if (selectedView === SelectedView.CallTree) {
|
||||
body = (
|
||||
<FlameGraphPane
|
||||
paneView={PaneView.CallTree}
|
||||
dataContainer={dataContainer}
|
||||
search={search}
|
||||
matchedLabels={matchedLabels}
|
||||
onTableSymbolClick={onTableSymbolClick}
|
||||
onTextAlignSelected={onTextAlignSelected}
|
||||
onTableSort={onTableSort}
|
||||
showFlameGraphOnly={showFlameGraphOnly}
|
||||
disableCollapsing={disableCollapsing}
|
||||
getExtraContextMenuButtons={getExtraContextMenuButtons}
|
||||
selectedView={selectedView}
|
||||
viewMode={viewMode}
|
||||
theme={theme}
|
||||
setSearch={setSearch}
|
||||
resetKey={resetKey}
|
||||
keepFocusOnDataChange={keepFocusOnDataChange}
|
||||
/>
|
||||
);
|
||||
} else if (selectedView === SelectedView.Multi) {
|
||||
// New view model: support split view with independent pane selections
|
||||
if (viewMode === ViewMode.Split) {
|
||||
if (vertical) {
|
||||
body = (
|
||||
<div>
|
||||
<div className={styles.verticalPaneContainer}>
|
||||
<FlameGraphPane
|
||||
key="left-pane"
|
||||
paneView={leftPaneView}
|
||||
dataContainer={dataContainer}
|
||||
search={search}
|
||||
matchedLabels={matchedLabels}
|
||||
onTableSymbolClick={onTableSymbolClick}
|
||||
onTextAlignSelected={onTextAlignSelected}
|
||||
onTableSort={onTableSort}
|
||||
showFlameGraphOnly={showFlameGraphOnly}
|
||||
disableCollapsing={disableCollapsing}
|
||||
getExtraContextMenuButtons={getExtraContextMenuButtons}
|
||||
selectedView={selectedView}
|
||||
viewMode={viewMode}
|
||||
theme={theme}
|
||||
setSearch={setSearch}
|
||||
resetKey={resetKey}
|
||||
keepFocusOnDataChange={keepFocusOnDataChange}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.verticalPaneContainer}>
|
||||
<FlameGraphPane
|
||||
key="right-pane"
|
||||
paneView={rightPaneView}
|
||||
dataContainer={dataContainer}
|
||||
search={search}
|
||||
matchedLabels={matchedLabels}
|
||||
onTableSymbolClick={onTableSymbolClick}
|
||||
onTextAlignSelected={onTextAlignSelected}
|
||||
onTableSort={onTableSort}
|
||||
showFlameGraphOnly={showFlameGraphOnly}
|
||||
disableCollapsing={disableCollapsing}
|
||||
getExtraContextMenuButtons={getExtraContextMenuButtons}
|
||||
selectedView={selectedView}
|
||||
viewMode={viewMode}
|
||||
theme={theme}
|
||||
setSearch={setSearch}
|
||||
resetKey={resetKey}
|
||||
keepFocusOnDataChange={keepFocusOnDataChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
body = (
|
||||
<div className={styles.horizontalContainer}>
|
||||
<div className={styles.horizontalPaneContainer}>
|
||||
<FlameGraphPane
|
||||
key="left-pane"
|
||||
paneView={leftPaneView}
|
||||
dataContainer={dataContainer}
|
||||
search={search}
|
||||
matchedLabels={matchedLabels}
|
||||
onTableSymbolClick={onTableSymbolClick}
|
||||
onTextAlignSelected={onTextAlignSelected}
|
||||
onTableSort={onTableSort}
|
||||
showFlameGraphOnly={showFlameGraphOnly}
|
||||
disableCollapsing={disableCollapsing}
|
||||
getExtraContextMenuButtons={getExtraContextMenuButtons}
|
||||
selectedView={selectedView}
|
||||
viewMode={viewMode}
|
||||
theme={theme}
|
||||
setSearch={setSearch}
|
||||
resetKey={resetKey}
|
||||
keepFocusOnDataChange={keepFocusOnDataChange}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.horizontalPaneContainer}>
|
||||
<FlameGraphPane
|
||||
key="right-pane"
|
||||
paneView={rightPaneView}
|
||||
dataContainer={dataContainer}
|
||||
search={search}
|
||||
matchedLabels={matchedLabels}
|
||||
onTableSymbolClick={onTableSymbolClick}
|
||||
onTextAlignSelected={onTextAlignSelected}
|
||||
onTableSort={onTableSort}
|
||||
showFlameGraphOnly={showFlameGraphOnly}
|
||||
disableCollapsing={disableCollapsing}
|
||||
getExtraContextMenuButtons={getExtraContextMenuButtons}
|
||||
selectedView={selectedView}
|
||||
viewMode={viewMode}
|
||||
theme={theme}
|
||||
setSearch={setSearch}
|
||||
resetKey={resetKey}
|
||||
keepFocusOnDataChange={keepFocusOnDataChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Single view mode
|
||||
body = (
|
||||
<div className={styles.horizontalContainer}>
|
||||
<div className={styles.horizontalTableContainer}>{table}</div>
|
||||
<div className={styles.horizontalGraphContainer}>{flameGraph}</div>
|
||||
<div className={styles.singlePaneContainer}>
|
||||
<FlameGraphPane
|
||||
key={`single-${singleView}`}
|
||||
paneView={singleView}
|
||||
dataContainer={dataContainer}
|
||||
search={search}
|
||||
matchedLabels={matchedLabels}
|
||||
onTableSymbolClick={onTableSymbolClick}
|
||||
onTextAlignSelected={onTextAlignSelected}
|
||||
onTableSort={onTableSort}
|
||||
showFlameGraphOnly={showFlameGraphOnly}
|
||||
disableCollapsing={disableCollapsing}
|
||||
getExtraContextMenuButtons={getExtraContextMenuButtons}
|
||||
selectedView={selectedView}
|
||||
viewMode={viewMode}
|
||||
theme={theme}
|
||||
setSearch={setSearch}
|
||||
resetKey={resetKey}
|
||||
keepFocusOnDataChange={keepFocusOnDataChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -292,25 +355,24 @@ const FlameGraphContainer = ({
|
||||
setSelectedView(view);
|
||||
onViewSelected?.(view);
|
||||
}}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
leftPaneView={leftPaneView}
|
||||
setLeftPaneView={setLeftPaneView}
|
||||
rightPaneView={rightPaneView}
|
||||
setRightPaneView={setRightPaneView}
|
||||
singleView={singleView}
|
||||
setSingleView={setSingleView}
|
||||
containerWidth={containerWidth}
|
||||
onReset={() => {
|
||||
resetFocus();
|
||||
resetSandwich();
|
||||
// Reset search and pane states when user clicks reset button
|
||||
setSearch('');
|
||||
setResetKey((k) => k + 1);
|
||||
}}
|
||||
textAlign={textAlign}
|
||||
onTextAlignChange={(align) => {
|
||||
setTextAlign(align);
|
||||
onTextAlignSelected?.(align);
|
||||
}}
|
||||
showResetButton={Boolean(focusedItemData || sandwichItem)}
|
||||
colorScheme={colorScheme}
|
||||
onColorSchemeChange={setColorScheme}
|
||||
showResetButton={Boolean(search)}
|
||||
stickyHeader={Boolean(stickyHeader)}
|
||||
extraHeaderElements={extraHeaderElements}
|
||||
vertical={vertical}
|
||||
isDiffMode={dataContainer.isDiffFlamegraph()}
|
||||
setCollapsedMap={setCollapsedMap}
|
||||
collapsedMap={collapsedMap}
|
||||
assistantContext={data && showAnalyzeWithAssistant ? getAssistantContextFromDataFrame(data) : undefined}
|
||||
/>
|
||||
)}
|
||||
@@ -321,18 +383,6 @@ const FlameGraphContainer = ({
|
||||
);
|
||||
};
|
||||
|
||||
function useColorScheme(dataContainer: FlameGraphDataContainer | undefined) {
|
||||
const defaultColorScheme = dataContainer?.isDiffFlamegraph() ? ColorSchemeDiff.Default : ColorScheme.PackageBased;
|
||||
const [colorScheme, setColorScheme] = useState<ColorScheme | ColorSchemeDiff>(defaultColorScheme);
|
||||
|
||||
// This makes sure that if we change the data to/from diff profile we reset the color scheme.
|
||||
useEffect(() => {
|
||||
setColorScheme(defaultColorScheme);
|
||||
}, [defaultColorScheme]);
|
||||
|
||||
return [colorScheme, setColorScheme] as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the search string it does a fuzzy search over all the unique labels, so we can highlight them later.
|
||||
*/
|
||||
@@ -420,12 +470,6 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
flexGrow: 1,
|
||||
}),
|
||||
|
||||
tableContainer: css({
|
||||
// This is not ideal for dashboard panel where it creates a double scroll. In a panel it should be 100% but then
|
||||
// in explore we need a specific height.
|
||||
height: 800,
|
||||
}),
|
||||
|
||||
horizontalContainer: css({
|
||||
label: 'horizontalContainer',
|
||||
display: 'flex',
|
||||
@@ -435,20 +479,20 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
width: '100%',
|
||||
}),
|
||||
|
||||
horizontalGraphContainer: css({
|
||||
flexBasis: '50%',
|
||||
}),
|
||||
|
||||
horizontalTableContainer: css({
|
||||
horizontalPaneContainer: css({
|
||||
label: 'horizontalPaneContainer',
|
||||
flexBasis: '50%',
|
||||
maxHeight: 800,
|
||||
}),
|
||||
|
||||
verticalGraphContainer: css({
|
||||
verticalPaneContainer: css({
|
||||
label: 'verticalPaneContainer',
|
||||
marginBottom: theme.spacing(1),
|
||||
height: 800,
|
||||
}),
|
||||
|
||||
verticalTableContainer: css({
|
||||
singlePaneContainer: css({
|
||||
label: 'singlePaneContainer',
|
||||
height: 800,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -3,9 +3,8 @@ import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import * as React from 'react';
|
||||
|
||||
import { CollapsedMap } from './FlameGraph/dataTransform';
|
||||
import FlameGraphHeader from './FlameGraphHeader';
|
||||
import { ColorScheme, SelectedView } from './types';
|
||||
import { PaneView, SelectedView, ViewMode } from './types';
|
||||
|
||||
jest.mock('@grafana/assistant', () => ({
|
||||
useAssistant: jest.fn().mockReturnValue({
|
||||
@@ -20,26 +19,30 @@ describe('FlameGraphHeader', () => {
|
||||
function setup(props: Partial<React.ComponentProps<typeof FlameGraphHeader>> = {}) {
|
||||
const setSearch = jest.fn();
|
||||
const setSelectedView = jest.fn();
|
||||
const setViewMode = jest.fn();
|
||||
const setLeftPaneView = jest.fn();
|
||||
const setRightPaneView = jest.fn();
|
||||
const setSingleView = jest.fn();
|
||||
const onReset = jest.fn();
|
||||
const onSchemeChange = jest.fn();
|
||||
|
||||
const renderResult = render(
|
||||
<FlameGraphHeader
|
||||
search={''}
|
||||
setSearch={setSearch}
|
||||
selectedView={SelectedView.Both}
|
||||
selectedView={SelectedView.Multi}
|
||||
setSelectedView={setSelectedView}
|
||||
viewMode={ViewMode.Split}
|
||||
setViewMode={setViewMode}
|
||||
leftPaneView={PaneView.TopTable}
|
||||
setLeftPaneView={setLeftPaneView}
|
||||
rightPaneView={PaneView.FlameGraph}
|
||||
setRightPaneView={setRightPaneView}
|
||||
singleView={PaneView.FlameGraph}
|
||||
setSingleView={setSingleView}
|
||||
containerWidth={1600}
|
||||
onReset={onReset}
|
||||
onTextAlignChange={jest.fn()}
|
||||
textAlign={'left'}
|
||||
showResetButton={true}
|
||||
colorScheme={ColorScheme.ValueBased}
|
||||
onColorSchemeChange={onSchemeChange}
|
||||
stickyHeader={false}
|
||||
isDiffMode={false}
|
||||
setCollapsedMap={() => {}}
|
||||
collapsedMap={new CollapsedMap()}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -50,7 +53,6 @@ describe('FlameGraphHeader', () => {
|
||||
setSearch,
|
||||
setSelectedView,
|
||||
onReset,
|
||||
onSchemeChange,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -70,27 +72,4 @@ describe('FlameGraphHeader', () => {
|
||||
await userEvent.click(resetButton);
|
||||
expect(handlers.onReset).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls on color scheme change when clicked', async () => {
|
||||
const { handlers } = setup();
|
||||
const changeButton = screen.getByLabelText(/Change color scheme/);
|
||||
expect(changeButton).toBeInTheDocument();
|
||||
await userEvent.click(changeButton);
|
||||
|
||||
const byPackageButton = screen.getByText(/By package name/);
|
||||
expect(byPackageButton).toBeInTheDocument();
|
||||
await userEvent.click(byPackageButton);
|
||||
|
||||
expect(handlers.onSchemeChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('shows diff color scheme switch when diff', async () => {
|
||||
setup({ isDiffMode: true });
|
||||
const changeButton = screen.getByLabelText(/Change color scheme/);
|
||||
expect(changeButton).toBeInTheDocument();
|
||||
await userEvent.click(changeButton);
|
||||
|
||||
expect(screen.getByText(/Default/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Color blind/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,30 +5,29 @@ import { useDebounce, usePrevious } from 'react-use';
|
||||
|
||||
import { ChatContextItem, OpenAssistantButton } from '@grafana/assistant';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Button, ButtonGroup, Dropdown, Input, Menu, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
||||
import { Button, Input, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { byPackageGradient, byValueGradient, diffColorBlindGradient, diffDefaultGradient } from './FlameGraph/colors';
|
||||
import { CollapsedMap } from './FlameGraph/dataTransform';
|
||||
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from './constants';
|
||||
import { ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
|
||||
import { PaneView, SelectedView, ViewMode } from './types';
|
||||
|
||||
type Props = {
|
||||
search: string;
|
||||
setSearch: (search: string) => void;
|
||||
selectedView: SelectedView;
|
||||
setSelectedView: (view: SelectedView) => void;
|
||||
viewMode: ViewMode;
|
||||
setViewMode: (mode: ViewMode) => void;
|
||||
leftPaneView: PaneView;
|
||||
setLeftPaneView: (view: PaneView) => void;
|
||||
rightPaneView: PaneView;
|
||||
setRightPaneView: (view: PaneView) => void;
|
||||
singleView: PaneView;
|
||||
setSingleView: (view: PaneView) => void;
|
||||
containerWidth: number;
|
||||
onReset: () => void;
|
||||
textAlign: TextAlign;
|
||||
onTextAlignChange: (align: TextAlign) => void;
|
||||
showResetButton: boolean;
|
||||
colorScheme: ColorScheme | ColorSchemeDiff;
|
||||
onColorSchemeChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
|
||||
stickyHeader: boolean;
|
||||
vertical?: boolean;
|
||||
isDiffMode: boolean;
|
||||
setCollapsedMap: (collapsedMap: CollapsedMap) => void;
|
||||
collapsedMap: CollapsedMap;
|
||||
|
||||
extraHeaderElements?: React.ReactNode;
|
||||
|
||||
@@ -40,19 +39,20 @@ const FlameGraphHeader = ({
|
||||
setSearch,
|
||||
selectedView,
|
||||
setSelectedView,
|
||||
viewMode,
|
||||
setViewMode,
|
||||
leftPaneView,
|
||||
setLeftPaneView,
|
||||
rightPaneView,
|
||||
setRightPaneView,
|
||||
singleView,
|
||||
setSingleView,
|
||||
containerWidth,
|
||||
onReset,
|
||||
textAlign,
|
||||
onTextAlignChange,
|
||||
showResetButton,
|
||||
colorScheme,
|
||||
onColorSchemeChange,
|
||||
stickyHeader,
|
||||
extraHeaderElements,
|
||||
vertical,
|
||||
isDiffMode,
|
||||
setCollapsedMap,
|
||||
collapsedMap,
|
||||
assistantContext,
|
||||
}: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
@@ -87,6 +87,25 @@ const FlameGraphHeader = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedView === SelectedView.Multi && viewMode === ViewMode.Split && (
|
||||
<div className={styles.middleContainer}>
|
||||
<RadioButtonGroup<PaneView>
|
||||
size="sm"
|
||||
options={paneViewOptions}
|
||||
value={leftPaneView}
|
||||
onChange={setLeftPaneView}
|
||||
className={styles.buttonSpacing}
|
||||
/>
|
||||
<RadioButtonGroup<PaneView>
|
||||
size="sm"
|
||||
options={paneViewOptions}
|
||||
value={rightPaneView}
|
||||
onChange={setRightPaneView}
|
||||
className={styles.buttonSpacing}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.rightContainer}>
|
||||
{!!assistantContext?.length && (
|
||||
<div className={styles.buttonSpacing}>
|
||||
@@ -111,129 +130,63 @@ const FlameGraphHeader = ({
|
||||
aria-label={'Reset focus and sandwich state'}
|
||||
/>
|
||||
)}
|
||||
<ColorSchemeButton value={colorScheme} onChange={onColorSchemeChange} isDiffMode={isDiffMode} />
|
||||
<ButtonGroup className={styles.buttonSpacing}>
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
fill={'outline'}
|
||||
size={'sm'}
|
||||
tooltip={'Expand all groups'}
|
||||
onClick={() => {
|
||||
setCollapsedMap(collapsedMap.setAllCollapsedStatus(false));
|
||||
}}
|
||||
aria-label={'Expand all groups'}
|
||||
icon={'angle-double-down'}
|
||||
disabled={selectedView === SelectedView.TopTable}
|
||||
{selectedView === SelectedView.Multi ? (
|
||||
<>
|
||||
{viewMode === ViewMode.Single && (
|
||||
<RadioButtonGroup<PaneView>
|
||||
size="sm"
|
||||
options={paneViewOptions}
|
||||
value={singleView}
|
||||
onChange={setSingleView}
|
||||
className={styles.buttonSpacing}
|
||||
/>
|
||||
)}
|
||||
<RadioButtonGroup<ViewMode>
|
||||
size="sm"
|
||||
options={viewModeOptions}
|
||||
value={viewMode}
|
||||
onChange={setViewMode}
|
||||
className={styles.buttonSpacing}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<RadioButtonGroup<SelectedView>
|
||||
size="sm"
|
||||
options={getViewOptions(containerWidth, vertical)}
|
||||
value={selectedView}
|
||||
onChange={setSelectedView}
|
||||
className={styles.buttonSpacing}
|
||||
/>
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
fill={'outline'}
|
||||
size={'sm'}
|
||||
tooltip={'Collapse all groups'}
|
||||
onClick={() => {
|
||||
setCollapsedMap(collapsedMap.setAllCollapsedStatus(true));
|
||||
}}
|
||||
aria-label={'Collapse all groups'}
|
||||
icon={'angle-double-up'}
|
||||
disabled={selectedView === SelectedView.TopTable}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<RadioButtonGroup<TextAlign>
|
||||
size="sm"
|
||||
disabled={selectedView === SelectedView.TopTable}
|
||||
options={alignOptions}
|
||||
value={textAlign}
|
||||
onChange={onTextAlignChange}
|
||||
className={styles.buttonSpacing}
|
||||
/>
|
||||
<RadioButtonGroup<SelectedView>
|
||||
size="sm"
|
||||
options={getViewOptions(containerWidth, vertical)}
|
||||
value={selectedView}
|
||||
onChange={setSelectedView}
|
||||
/>
|
||||
)}
|
||||
{extraHeaderElements && <div className={styles.extraElements}>{extraHeaderElements}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type ColorSchemeButtonProps = {
|
||||
value: ColorScheme | ColorSchemeDiff;
|
||||
onChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
|
||||
isDiffMode: boolean;
|
||||
};
|
||||
function ColorSchemeButton(props: ColorSchemeButtonProps) {
|
||||
// TODO: probably create separate getStyles
|
||||
const styles = useStyles2(getStyles);
|
||||
let menu = (
|
||||
<Menu>
|
||||
<Menu.Item label="By package name" onClick={() => props.onChange(ColorScheme.PackageBased)} />
|
||||
<Menu.Item label="By value" onClick={() => props.onChange(ColorScheme.ValueBased)} />
|
||||
</Menu>
|
||||
);
|
||||
const viewModeOptions: Array<SelectableValue<ViewMode>> = [
|
||||
{ value: ViewMode.Single, label: 'Single', description: 'Single view' },
|
||||
{ value: ViewMode.Split, label: 'Split', description: 'Split view' },
|
||||
];
|
||||
|
||||
// Show a bit different gradient as a way to indicate selected value
|
||||
const colorDotStyle =
|
||||
{
|
||||
[ColorScheme.ValueBased]: styles.colorDotByValue,
|
||||
[ColorScheme.PackageBased]: styles.colorDotByPackage,
|
||||
[ColorSchemeDiff.DiffColorBlind]: styles.colorDotDiffColorBlind,
|
||||
[ColorSchemeDiff.Default]: styles.colorDotDiffDefault,
|
||||
}[props.value] || styles.colorDotByValue;
|
||||
|
||||
let contents = <span className={cx(styles.colorDot, colorDotStyle)} />;
|
||||
|
||||
if (props.isDiffMode) {
|
||||
menu = (
|
||||
<Menu>
|
||||
<Menu.Item label="Default (green to red)" onClick={() => props.onChange(ColorSchemeDiff.Default)} />
|
||||
<Menu.Item label="Color blind (blue to red)" onClick={() => props.onChange(ColorSchemeDiff.DiffColorBlind)} />
|
||||
</Menu>
|
||||
);
|
||||
|
||||
contents = (
|
||||
<div className={cx(styles.colorDotDiff, colorDotStyle)}>
|
||||
<div>-100% (removed)</div>
|
||||
<div>0%</div>
|
||||
<div>+100% (added)</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown overlay={menu}>
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
fill={'outline'}
|
||||
size={'sm'}
|
||||
tooltip={'Change color scheme'}
|
||||
onClick={() => {}}
|
||||
className={styles.buttonSpacing}
|
||||
aria-label={'Change color scheme'}
|
||||
>
|
||||
{contents}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
const alignOptions: Array<SelectableValue<TextAlign>> = [
|
||||
{ value: 'left', description: 'Align text left', icon: 'align-left' },
|
||||
{ value: 'right', description: 'Align text right', icon: 'align-right' },
|
||||
const paneViewOptions: Array<SelectableValue<PaneView>> = [
|
||||
{ value: PaneView.TopTable, label: 'Top Table' },
|
||||
{ value: PaneView.FlameGraph, label: 'Flame Graph' },
|
||||
{ value: PaneView.CallTree, label: 'Call Tree' },
|
||||
];
|
||||
|
||||
function getViewOptions(width: number, vertical?: boolean): Array<SelectableValue<SelectedView>> {
|
||||
let viewOptions: Array<{ value: SelectedView; label: string; description: string }> = [
|
||||
{ value: SelectedView.TopTable, label: 'Top Table', description: 'Only show top table' },
|
||||
{ value: SelectedView.FlameGraph, label: 'Flame Graph', description: 'Only show flame graph' },
|
||||
{ value: SelectedView.CallTree, label: 'Call Tree', description: 'Only show call tree' },
|
||||
];
|
||||
|
||||
if (width >= MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH || vertical) {
|
||||
viewOptions.push({
|
||||
value: SelectedView.Both,
|
||||
label: 'Both',
|
||||
description: 'Show both the top table and flame graph',
|
||||
value: SelectedView.Multi,
|
||||
label: 'Multi',
|
||||
description: 'Show split or single view with multiple visualizations',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -273,10 +226,12 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
width: '100%',
|
||||
top: 0,
|
||||
gap: theme.spacing(1),
|
||||
marginTop: theme.spacing(1),
|
||||
position: 'relative',
|
||||
}),
|
||||
stickyHeader: css({
|
||||
zIndex: theme.zIndex.navbarFixed,
|
||||
@@ -285,10 +240,20 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
}),
|
||||
inputContainer: css({
|
||||
label: 'inputContainer',
|
||||
flexGrow: 1,
|
||||
flexGrow: 0,
|
||||
minWidth: '150px',
|
||||
maxWidth: '350px',
|
||||
}),
|
||||
middleContainer: css({
|
||||
label: 'middleContainer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: theme.spacing(1),
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
}),
|
||||
rightContainer: css({
|
||||
label: 'rightContainer',
|
||||
display: 'flex',
|
||||
@@ -309,44 +274,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
padding: '0 5px',
|
||||
color: theme.colors.text.disabled,
|
||||
}),
|
||||
colorDot: css({
|
||||
label: 'colorDot',
|
||||
display: 'inline-block',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
borderRadius: theme.shape.radius.circle,
|
||||
}),
|
||||
colorDotDiff: css({
|
||||
label: 'colorDotDiff',
|
||||
display: 'flex',
|
||||
width: '200px',
|
||||
height: '12px',
|
||||
color: 'white',
|
||||
fontSize: 9,
|
||||
lineHeight: 1.3,
|
||||
fontWeight: 300,
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 2px',
|
||||
// We have a specific sizing for this so probably makes sense to use hardcoded value here
|
||||
// eslint-disable-next-line @grafana/no-border-radius-literal
|
||||
borderRadius: '2px',
|
||||
}),
|
||||
colorDotByValue: css({
|
||||
label: 'colorDotByValue',
|
||||
background: byValueGradient,
|
||||
}),
|
||||
colorDotByPackage: css({
|
||||
label: 'colorDotByPackage',
|
||||
background: byPackageGradient,
|
||||
}),
|
||||
colorDotDiffDefault: css({
|
||||
label: 'colorDotDiffDefault',
|
||||
background: diffDefaultGradient,
|
||||
}),
|
||||
colorDotDiffColorBlind: css({
|
||||
label: 'colorDotDiffColorBlind',
|
||||
background: diffColorBlindGradient,
|
||||
}),
|
||||
extraElements: css({
|
||||
label: 'extraElements',
|
||||
marginLeft: theme.spacing(1),
|
||||
|
||||
269
packages/grafana-flamegraph/src/FlameGraphPane.tsx
Normal file
269
packages/grafana-flamegraph/src/FlameGraphPane.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2, escapeStringForRegex } from '@grafana/data';
|
||||
|
||||
import FlameGraphCallTreeContainer from './CallTree/FlameGraphCallTreeContainer';
|
||||
import FlameGraph from './FlameGraph/FlameGraph';
|
||||
import { GetExtraContextMenuButtonsFunction } from './FlameGraph/FlameGraphContextMenu';
|
||||
import { FlameGraphDataContainer } from './FlameGraph/dataTransform';
|
||||
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
|
||||
import { ClickedItemData, ColorScheme, ColorSchemeDiff, PaneView, SelectedView, TextAlign, ViewMode } from './types';
|
||||
|
||||
export type FlameGraphPaneProps = {
|
||||
paneView: PaneView;
|
||||
dataContainer: FlameGraphDataContainer;
|
||||
search: string;
|
||||
matchedLabels: Set<string> | undefined;
|
||||
onTableSymbolClick?: (symbol: string) => void;
|
||||
onTextAlignSelected?: (align: string) => void;
|
||||
onTableSort?: (sort: string) => void;
|
||||
showFlameGraphOnly?: boolean;
|
||||
disableCollapsing?: boolean;
|
||||
getExtraContextMenuButtons?: GetExtraContextMenuButtonsFunction;
|
||||
selectedView: SelectedView;
|
||||
viewMode: ViewMode;
|
||||
theme: GrafanaTheme2;
|
||||
setSearch: (search: string) => void;
|
||||
/** When this key changes, the pane's internal state (focus, sandwich, etc.) will be reset */
|
||||
resetKey?: number;
|
||||
/** Whether to preserve focus when the data changes */
|
||||
keepFocusOnDataChange?: boolean;
|
||||
};
|
||||
|
||||
const FlameGraphPane = ({
|
||||
paneView,
|
||||
dataContainer,
|
||||
search,
|
||||
matchedLabels,
|
||||
onTableSymbolClick,
|
||||
onTextAlignSelected,
|
||||
onTableSort,
|
||||
showFlameGraphOnly,
|
||||
disableCollapsing,
|
||||
getExtraContextMenuButtons,
|
||||
selectedView,
|
||||
viewMode,
|
||||
theme,
|
||||
setSearch,
|
||||
resetKey,
|
||||
keepFocusOnDataChange,
|
||||
}: FlameGraphPaneProps) => {
|
||||
// Pane-specific state - each instance maintains its own
|
||||
const [focusedItemData, setFocusedItemData] = useState<ClickedItemData>();
|
||||
const [rangeMin, setRangeMin] = useState(0);
|
||||
const [rangeMax, setRangeMax] = useState(1);
|
||||
const [textAlign, setTextAlign] = useState<TextAlign>('left');
|
||||
const [sandwichItem, setSandwichItem] = useState<string>();
|
||||
// Initialize collapsedMap from dataContainer to ensure collapsed groups are shown correctly on first render
|
||||
const [collapsedMap, setCollapsedMap] = useState(() => dataContainer.getCollapsedMap());
|
||||
const [colorScheme, setColorScheme] = useColorScheme(dataContainer);
|
||||
|
||||
const styles = useMemo(() => getStyles(theme), [theme]);
|
||||
|
||||
// Re-initialize collapsed map when dataContainer changes (e.g., new data loaded)
|
||||
// Using useLayoutEffect to ensure collapsed state is applied before browser paint
|
||||
useLayoutEffect(() => {
|
||||
setCollapsedMap(dataContainer.getCollapsedMap());
|
||||
}, [dataContainer]);
|
||||
|
||||
// Reset internal state when resetKey changes (triggered by parent's reset button)
|
||||
useEffect(() => {
|
||||
if (resetKey !== undefined && resetKey > 0) {
|
||||
setFocusedItemData(undefined);
|
||||
setRangeMin(0);
|
||||
setRangeMax(1);
|
||||
setSandwichItem(undefined);
|
||||
}
|
||||
}, [resetKey]);
|
||||
|
||||
// Handle focus preservation or reset when data changes
|
||||
useEffect(() => {
|
||||
if (!keepFocusOnDataChange) {
|
||||
setFocusedItemData(undefined);
|
||||
setRangeMin(0);
|
||||
setRangeMax(1);
|
||||
setSandwichItem(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataContainer && focusedItemData) {
|
||||
const item = dataContainer.getNodesWithLabel(focusedItemData.label)?.[0];
|
||||
|
||||
if (item) {
|
||||
setFocusedItemData({ ...focusedItemData, item });
|
||||
|
||||
const levels = dataContainer.getLevels();
|
||||
const totalViewTicks = levels.length ? levels[0][0].value : 0;
|
||||
setRangeMin(item.start / totalViewTicks);
|
||||
setRangeMax((item.start + item.value) / totalViewTicks);
|
||||
} else {
|
||||
setFocusedItemData({
|
||||
...focusedItemData,
|
||||
item: {
|
||||
start: 0,
|
||||
value: 0,
|
||||
itemIndexes: [],
|
||||
children: [],
|
||||
level: 0,
|
||||
},
|
||||
});
|
||||
|
||||
setRangeMin(0);
|
||||
setRangeMax(1);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dataContainer, keepFocusOnDataChange]);
|
||||
|
||||
const resetFocus = useCallback(() => {
|
||||
setFocusedItemData(undefined);
|
||||
setRangeMin(0);
|
||||
setRangeMax(1);
|
||||
}, []);
|
||||
|
||||
const resetSandwich = useCallback(() => {
|
||||
setSandwichItem(undefined);
|
||||
}, []);
|
||||
|
||||
const onSymbolClick = useCallback(
|
||||
(symbol: string) => {
|
||||
const anchored = `^${escapeStringForRegex(symbol)}$`;
|
||||
if (search === anchored) {
|
||||
setSearch('');
|
||||
} else {
|
||||
onTableSymbolClick?.(symbol);
|
||||
setSearch(anchored);
|
||||
resetFocus();
|
||||
}
|
||||
},
|
||||
[search, setSearch, resetFocus, onTableSymbolClick]
|
||||
);
|
||||
|
||||
// Separate callback for CallTree that doesn't trigger search
|
||||
const onCallTreeSymbolClick = useCallback(
|
||||
(symbol: string) => {
|
||||
onTableSymbolClick?.(symbol);
|
||||
},
|
||||
[onTableSymbolClick]
|
||||
);
|
||||
|
||||
// Search callback for CallTree search button
|
||||
const onCallTreeSearch = useCallback(
|
||||
(symbol: string) => {
|
||||
const anchored = `^${escapeStringForRegex(symbol)}$`;
|
||||
if (search === anchored) {
|
||||
setSearch('');
|
||||
} else {
|
||||
onTableSymbolClick?.(symbol);
|
||||
setSearch(anchored);
|
||||
resetFocus();
|
||||
}
|
||||
},
|
||||
[search, setSearch, resetFocus, onTableSymbolClick]
|
||||
);
|
||||
|
||||
const isInSplitView = selectedView === SelectedView.Multi && viewMode === ViewMode.Split;
|
||||
const isCallTreeInSplitView = isInSplitView && paneView === PaneView.CallTree;
|
||||
|
||||
switch (paneView) {
|
||||
case PaneView.TopTable:
|
||||
return (
|
||||
<div className={styles.tableContainer}>
|
||||
<FlameGraphTopTableContainer
|
||||
data={dataContainer}
|
||||
onSymbolClick={onSymbolClick}
|
||||
search={search}
|
||||
matchedLabels={matchedLabels}
|
||||
sandwichItem={sandwichItem}
|
||||
onSandwich={setSandwichItem}
|
||||
onSearch={(str) => {
|
||||
if (!str) {
|
||||
setSearch('');
|
||||
return;
|
||||
}
|
||||
setSearch(`^${escapeStringForRegex(str)}$`);
|
||||
}}
|
||||
onTableSort={onTableSort}
|
||||
colorScheme={colorScheme}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case PaneView.FlameGraph:
|
||||
default:
|
||||
return (
|
||||
<FlameGraph
|
||||
data={dataContainer}
|
||||
rangeMin={rangeMin}
|
||||
rangeMax={rangeMax}
|
||||
matchedLabels={matchedLabels}
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
onItemFocused={(data) => setFocusedItemData(data)}
|
||||
focusedItemData={focusedItemData}
|
||||
textAlign={textAlign}
|
||||
onTextAlignChange={(align) => {
|
||||
setTextAlign(align);
|
||||
onTextAlignSelected?.(align);
|
||||
}}
|
||||
sandwichItem={sandwichItem}
|
||||
onSandwich={(label: string) => {
|
||||
resetFocus();
|
||||
setSandwichItem(label);
|
||||
}}
|
||||
onFocusPillClick={resetFocus}
|
||||
onSandwichPillClick={resetSandwich}
|
||||
colorScheme={colorScheme}
|
||||
onColorSchemeChange={setColorScheme}
|
||||
isDiffMode={dataContainer.isDiffFlamegraph()}
|
||||
showFlameGraphOnly={showFlameGraphOnly}
|
||||
collapsing={!disableCollapsing}
|
||||
getExtraContextMenuButtons={getExtraContextMenuButtons}
|
||||
selectedView={selectedView}
|
||||
search={search}
|
||||
collapsedMap={collapsedMap}
|
||||
setCollapsedMap={setCollapsedMap}
|
||||
/>
|
||||
);
|
||||
case PaneView.CallTree:
|
||||
return (
|
||||
<div className={styles.tableContainer}>
|
||||
<FlameGraphCallTreeContainer
|
||||
data={dataContainer}
|
||||
onSymbolClick={onCallTreeSymbolClick}
|
||||
sandwichItem={sandwichItem}
|
||||
onSandwich={setSandwichItem}
|
||||
onTableSort={onTableSort}
|
||||
colorScheme={colorScheme}
|
||||
search={search}
|
||||
compact={isCallTreeInSplitView}
|
||||
onSearch={onCallTreeSearch}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function useColorScheme(dataContainer: FlameGraphDataContainer | undefined) {
|
||||
const defaultColorScheme = dataContainer?.isDiffFlamegraph() ? ColorSchemeDiff.Default : ColorScheme.PackageBased;
|
||||
const [colorScheme, setColorScheme] = useState<ColorScheme | ColorSchemeDiff>(defaultColorScheme);
|
||||
|
||||
// This makes sure that if we change the data to/from diff profile we reset the color scheme.
|
||||
useEffect(() => {
|
||||
setColorScheme(defaultColorScheme);
|
||||
}, [defaultColorScheme]);
|
||||
|
||||
return [colorScheme, setColorScheme] as const;
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
tableContainer: css({
|
||||
// This is not ideal for dashboard panel where it creates a double scroll. In a panel it should be 100% but then
|
||||
// in explore we need a specific height.
|
||||
height: 800,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export default FlameGraphPane;
|
||||
@@ -1,3 +1,4 @@
|
||||
export { default as FlameGraph, type Props } from './FlameGraphContainer';
|
||||
export { default as FlameGraphCallTreeContainer } from './CallTree/FlameGraphCallTreeContainer';
|
||||
export { checkFields, getMessageCheckFieldsResult } from './FlameGraph/dataTransform';
|
||||
export { data } from './FlameGraph/testData/dataNestedSet';
|
||||
|
||||
@@ -20,7 +20,19 @@ export enum SampleUnit {
|
||||
export enum SelectedView {
|
||||
TopTable = 'topTable',
|
||||
FlameGraph = 'flameGraph',
|
||||
Both = 'both',
|
||||
Multi = 'multi',
|
||||
CallTree = 'callTree',
|
||||
}
|
||||
|
||||
export enum ViewMode {
|
||||
Single = 'single',
|
||||
Split = 'split',
|
||||
}
|
||||
|
||||
export enum PaneView {
|
||||
TopTable = 'topTable',
|
||||
FlameGraph = 'flameGraph',
|
||||
CallTree = 'callTree',
|
||||
}
|
||||
|
||||
export interface TableData {
|
||||
|
||||
@@ -1,14 +1,40 @@
|
||||
import { Decorator } from '@storybook/react';
|
||||
import { useEffect } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { getThemeById, ThemeContext } from '@grafana/data';
|
||||
import { GlobalStyles } from '@grafana/ui';
|
||||
import { createTheme, getThemeById, ThemeContext } from '@grafana/data';
|
||||
import { GlobalStyles, PortalContainer } from '@grafana/ui';
|
||||
|
||||
interface ThemeableStoryProps {
|
||||
themeId: string;
|
||||
themeId?: string;
|
||||
}
|
||||
const ThemeableStory = ({ children, themeId }: React.PropsWithChildren<ThemeableStoryProps>) => {
|
||||
const theme = getThemeById(themeId);
|
||||
// Always ensure we have a valid theme
|
||||
const theme = React.useMemo(() => {
|
||||
const id = themeId || 'dark';
|
||||
let resolvedTheme = getThemeById(id);
|
||||
|
||||
// If getThemeById returns undefined, create a default theme
|
||||
if (!resolvedTheme) {
|
||||
console.warn(`Theme '${id}' not found, using default theme`);
|
||||
resolvedTheme = createTheme({ colors: { mode: id === 'light' ? 'light' : 'dark' } });
|
||||
}
|
||||
|
||||
console.log('withTheme: resolved theme', { id, hasTheme: !!resolvedTheme, hasSpacing: !!resolvedTheme?.spacing });
|
||||
return resolvedTheme;
|
||||
}, [themeId]);
|
||||
|
||||
// Apply theme to document root for Portals
|
||||
useEffect(() => {
|
||||
if (!theme) return;
|
||||
|
||||
document.body.style.setProperty('--theme-background', theme.colors.background.primary);
|
||||
}, [theme]);
|
||||
|
||||
if (!theme) {
|
||||
console.error('withTheme: No theme available!');
|
||||
return null;
|
||||
}
|
||||
|
||||
const css = `
|
||||
#storybook-root {
|
||||
@@ -23,6 +49,7 @@ const ThemeableStory = ({ children, themeId }: React.PropsWithChildren<Themeable
|
||||
return (
|
||||
<ThemeContext.Provider value={theme}>
|
||||
<GlobalStyles />
|
||||
<PortalContainer />
|
||||
|
||||
<style>{css}</style>
|
||||
{children}
|
||||
@@ -33,4 +60,4 @@ const ThemeableStory = ({ children, themeId }: React.PropsWithChildren<Themeable
|
||||
export const withTheme =
|
||||
(): Decorator =>
|
||||
// eslint-disable-next-line react/display-name
|
||||
(story, context) => <ThemeableStory themeId={context.globals.theme}>{story()}</ThemeableStory>;
|
||||
(story, context) => <ThemeableStory themeId={context.globals?.theme}>{story()}</ThemeableStory>;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import * as common from '@grafana/schema';
|
||||
|
||||
export const pluginVersion = "12.4.0-pre";
|
||||
export const pluginVersion = "%VERSION%";
|
||||
|
||||
export type BucketAggregation = (DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -24,6 +24,24 @@ func TestMain(m *testing.M) {
|
||||
testsuite.Run(m)
|
||||
}
|
||||
|
||||
// mockElasticsearchHandler returns a handler that mocks Elasticsearch endpoints.
|
||||
// It responds to GET / with cluster info (required for datasource initialization)
|
||||
// and returns 401 Unauthorized for all other requests.
|
||||
func mockElasticsearchHandler(onRequest func(r *http.Request)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"version":{"build_flavor":"default","number":"8.0.0"}}`))
|
||||
default:
|
||||
if onRequest != nil {
|
||||
onRequest(r)
|
||||
}
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationElasticsearch(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
@@ -35,9 +53,8 @@ func TestIntegrationElasticsearch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
var outgoingRequest *http.Request
|
||||
outgoingServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
outgoingServer := httptest.NewServer(mockElasticsearchHandler(func(r *http.Request) {
|
||||
outgoingRequest = r
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}))
|
||||
t.Cleanup(outgoingServer.Close)
|
||||
|
||||
|
||||
@@ -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": "",
|
||||
@@ -639,7 +639,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaDependency": "\u003e=11.6.0",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
@@ -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
|
||||
}
|
||||
@@ -92,7 +92,7 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
|
||||
}, nil
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%v/v3/projects/%v/metricDescriptors", dsInfo.services[cloudMonitor].url, defaultProject)
|
||||
url := fmt.Sprintf("%s/v3/projects/%s/metricDescriptors", dsInfo.services[cloudMonitor].url, defaultProject)
|
||||
request, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -139,6 +139,7 @@ type datasourceInfo struct {
|
||||
defaultProject string
|
||||
clientEmail string
|
||||
tokenUri string
|
||||
universeDomain string
|
||||
services map[string]datasourceService
|
||||
privateKey string
|
||||
usingImpersonation bool
|
||||
@@ -150,6 +151,7 @@ type datasourceJSONData struct {
|
||||
DefaultProject string `json:"defaultProject"`
|
||||
ClientEmail string `json:"clientEmail"`
|
||||
TokenURI string `json:"tokenUri"`
|
||||
UniverseDomain string `json:"universeDomain"`
|
||||
UsingImpersonation bool `json:"usingImpersonation"`
|
||||
ServiceAccountToImpersonate string `json:"serviceAccountToImpersonate"`
|
||||
}
|
||||
@@ -179,6 +181,7 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
|
||||
defaultProject: jsonData.DefaultProject,
|
||||
clientEmail: jsonData.ClientEmail,
|
||||
tokenUri: jsonData.TokenURI,
|
||||
universeDomain: jsonData.UniverseDomain,
|
||||
usingImpersonation: jsonData.UsingImpersonation,
|
||||
serviceAccountToImpersonate: jsonData.ServiceAccountToImpersonate,
|
||||
services: map[string]datasourceService{},
|
||||
@@ -194,13 +197,13 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for name, info := range routes {
|
||||
for name := range routes {
|
||||
client, err := newHTTPClient(dsInfo, opts, &httpClientProvider, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dsInfo.services[name] = datasourceService{
|
||||
url: info.url,
|
||||
url: buildURL(name, dsInfo.universeDomain),
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ type routeInfo struct {
|
||||
var routes = map[string]routeInfo{
|
||||
cloudMonitor: {
|
||||
method: "GET",
|
||||
url: "https://monitoring.googleapis.com",
|
||||
url: "https://monitoring.",
|
||||
scopes: []string{cloudMonitorScope},
|
||||
},
|
||||
resourceManager: {
|
||||
method: "GET",
|
||||
url: "https://cloudresourcemanager.googleapis.com",
|
||||
url: "https://cloudresourcemanager.",
|
||||
scopes: []string{resourceManagerScope},
|
||||
},
|
||||
}
|
||||
@@ -68,6 +68,13 @@ func getMiddleware(model *datasourceInfo, routePath string) (httpclient.Middlewa
|
||||
return tokenprovider.AuthMiddleware(provider), nil
|
||||
}
|
||||
|
||||
func buildURL(route string, universeDomain string) string {
|
||||
if universeDomain == "" {
|
||||
universeDomain = "googleapis.com"
|
||||
}
|
||||
return routes[route].url + universeDomain
|
||||
}
|
||||
|
||||
func newHTTPClient(model *datasourceInfo, opts httpclient.Options, clientProvider *httpclient.Provider, route string) (*http.Client, error) {
|
||||
m, err := getMiddleware(model, route)
|
||||
if err != nil {
|
||||
|
||||
@@ -111,7 +111,7 @@ func Test_setRequestVariables(t *testing.T) {
|
||||
im: &fakeInstance{
|
||||
services: map[string]datasourceService{
|
||||
cloudMonitor: {
|
||||
url: routes[cloudMonitor].url,
|
||||
url: buildURL(cloudMonitor, "googleapis.com"),
|
||||
client: &http.Client{},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,8 +3,8 @@ package elasticsearch
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
// addDateHistogramAgg adds a date histogram aggregation to the aggregation builder
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
// Used in logging to mark a stage
|
||||
@@ -35,6 +34,7 @@ type DatasourceInfo struct {
|
||||
Interval string
|
||||
MaxConcurrentShardRequests int64
|
||||
IncludeFrozen bool
|
||||
ClusterInfo ClusterInfo
|
||||
}
|
||||
|
||||
type ConfiguredFields struct {
|
||||
@@ -159,7 +159,7 @@ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearch
|
||||
resSpan.End()
|
||||
}()
|
||||
|
||||
improvedParsingEnabled := isFeatureEnabled(c.ctx, featuremgmt.FlagElasticsearchImprovedParsing)
|
||||
improvedParsingEnabled := isFeatureEnabled(c.ctx, "elasticsearchImprovedParsing")
|
||||
msr, err := c.parser.parseMultiSearchResponse(res.Body, improvedParsingEnabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -197,7 +197,11 @@ func (c *baseClientImpl) createMultiSearchRequests(searchRequests []*SearchReque
|
||||
|
||||
func (c *baseClientImpl) getMultiSearchQueryParameters() string {
|
||||
var qs []string
|
||||
qs = append(qs, fmt.Sprintf("max_concurrent_shard_requests=%d", c.ds.MaxConcurrentShardRequests))
|
||||
// if the build flavor is not serverless, we can use the max concurrent shard requests
|
||||
// this is because serverless clusters do not support max concurrent shard requests
|
||||
if !c.ds.ClusterInfo.IsServerless() && c.ds.MaxConcurrentShardRequests > 0 {
|
||||
qs = append(qs, fmt.Sprintf("max_concurrent_shard_requests=%d", c.ds.MaxConcurrentShardRequests))
|
||||
}
|
||||
|
||||
if c.ds.IncludeFrozen {
|
||||
qs = append(qs, "ignore_throttled=false")
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
func TestClient_ExecuteMultisearch(t *testing.T) {
|
||||
|
||||
51
pkg/tsdb/elasticsearch/client/cluster_info.go
Normal file
51
pkg/tsdb/elasticsearch/client/cluster_info.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package es
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type VersionInfo struct {
|
||||
BuildFlavor string `json:"build_flavor"`
|
||||
}
|
||||
|
||||
// ClusterInfo represents Elasticsearch cluster information returned from the root endpoint.
|
||||
// It is used to determine cluster capabilities and configuration like whether the cluster is serverless.
|
||||
type ClusterInfo struct {
|
||||
Version VersionInfo `json:"version"`
|
||||
}
|
||||
|
||||
const (
|
||||
BuildFlavorServerless = "serverless"
|
||||
)
|
||||
|
||||
// GetClusterInfo fetches cluster information from the Elasticsearch root endpoint.
|
||||
// It returns the cluster build flavor which is used to determine if the cluster is serverless.
|
||||
func GetClusterInfo(httpCli *http.Client, url string) (clusterInfo ClusterInfo, err error) {
|
||||
resp, err := httpCli.Get(url)
|
||||
if err != nil {
|
||||
return ClusterInfo{}, fmt.Errorf("error getting ES cluster info: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return ClusterInfo{}, fmt.Errorf("unexpected status code %d getting ES cluster info", resp.StatusCode)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if closeErr := resp.Body.Close(); closeErr != nil && err == nil {
|
||||
err = fmt.Errorf("error closing response body: %w", closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&clusterInfo)
|
||||
if err != nil {
|
||||
return ClusterInfo{}, fmt.Errorf("error decoding ES cluster info: %w", err)
|
||||
}
|
||||
|
||||
return clusterInfo, nil
|
||||
}
|
||||
|
||||
func (ci ClusterInfo) IsServerless() bool {
|
||||
return ci.Version.BuildFlavor == BuildFlavorServerless
|
||||
}
|
||||
188
pkg/tsdb/elasticsearch/client/cluster_info_test.go
Normal file
188
pkg/tsdb/elasticsearch/client/cluster_info_test.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package es
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetClusterInfo(t *testing.T) {
|
||||
t.Run("Should successfully get cluster info", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
_, err := rw.Write([]byte(`{
|
||||
"name": "test-cluster",
|
||||
"cluster_name": "elasticsearch",
|
||||
"cluster_uuid": "abc123",
|
||||
"version": {
|
||||
"number": "8.0.0",
|
||||
"build_flavor": "default",
|
||||
"build_type": "tar",
|
||||
"build_hash": "abc123",
|
||||
"build_date": "2023-01-01T00:00:00.000Z",
|
||||
"build_snapshot": false,
|
||||
"lucene_version": "9.0.0"
|
||||
}
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, clusterInfo)
|
||||
assert.Equal(t, "default", clusterInfo.Version.BuildFlavor)
|
||||
})
|
||||
|
||||
t.Run("Should successfully get serverless cluster info", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
_, err := rw.Write([]byte(`{
|
||||
"name": "serverless-cluster",
|
||||
"cluster_name": "elasticsearch",
|
||||
"cluster_uuid": "def456",
|
||||
"version": {
|
||||
"number": "8.11.0",
|
||||
"build_flavor": "serverless",
|
||||
"build_type": "docker",
|
||||
"build_hash": "def456",
|
||||
"build_date": "2023-11-01T00:00:00.000Z",
|
||||
"build_snapshot": false,
|
||||
"lucene_version": "9.8.0"
|
||||
}
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, clusterInfo)
|
||||
assert.Equal(t, "serverless", clusterInfo.Version.BuildFlavor)
|
||||
assert.True(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should return error when HTTP request fails", func(t *testing.T) {
|
||||
clusterInfo, err := GetClusterInfo(http.DefaultClient, "http://invalid-url-that-does-not-exist.local:9999")
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ClusterInfo{}, clusterInfo)
|
||||
assert.Contains(t, err.Error(), "error getting ES cluster info")
|
||||
})
|
||||
|
||||
t.Run("Should return error when response body is invalid JSON", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
_, err := rw.Write([]byte(`{"invalid json`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ClusterInfo{}, clusterInfo)
|
||||
assert.Contains(t, err.Error(), "error decoding ES cluster info")
|
||||
})
|
||||
|
||||
t.Run("Should handle empty version object", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
_, err := rw.Write([]byte(`{
|
||||
"name": "test-cluster",
|
||||
"version": {}
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ClusterInfo{}, clusterInfo)
|
||||
assert.Equal(t, "", clusterInfo.Version.BuildFlavor)
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should handle HTTP error status codes", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
_, err := rw.Write([]byte(`{"error": "Unauthorized"}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ClusterInfo{}, clusterInfo)
|
||||
assert.Contains(t, err.Error(), "unexpected status code 401 getting ES cluster info")
|
||||
})
|
||||
}
|
||||
|
||||
func TestClusterInfo_IsServerless(t *testing.T) {
|
||||
t.Run("Should return true when build_flavor is serverless", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{
|
||||
Version: VersionInfo{
|
||||
BuildFlavor: BuildFlavorServerless,
|
||||
},
|
||||
}
|
||||
|
||||
assert.True(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should return false when build_flavor is default", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{
|
||||
Version: VersionInfo{
|
||||
BuildFlavor: "default",
|
||||
},
|
||||
}
|
||||
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should return false when build_flavor is empty", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{
|
||||
Version: VersionInfo{
|
||||
BuildFlavor: "",
|
||||
},
|
||||
}
|
||||
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should return false when build_flavor is unknown value", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{
|
||||
Version: VersionInfo{
|
||||
BuildFlavor: "unknown",
|
||||
},
|
||||
}
|
||||
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("should return false when cluster info is empty", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{}
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
func TestSearchRequest(t *testing.T) {
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
// processQuery processes a single query and adds it to the multi-search request builder
|
||||
|
||||
@@ -3,7 +3,7 @@ package elasticsearch
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
// setFloatPath converts a string value at the specified path to float64
|
||||
|
||||
@@ -88,6 +88,14 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins
|
||||
httpCliOpts.SigV4.Service = "es"
|
||||
}
|
||||
|
||||
apiKeyAuth, ok := jsonData["apiKeyAuth"].(bool)
|
||||
if ok && apiKeyAuth {
|
||||
apiKey := settings.DecryptedSecureJSONData["apiKey"]
|
||||
if apiKey != "" {
|
||||
httpCliOpts.Header.Add("Authorization", "ApiKey "+apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
httpCli, err := httpClientProvider.New(httpCliOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -151,6 +159,11 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins
|
||||
includeFrozen = false
|
||||
}
|
||||
|
||||
clusterInfo, err := es.GetClusterInfo(httpCli, settings.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configuredFields := es.ConfiguredFields{
|
||||
TimeField: timeField,
|
||||
LogLevelField: logLevelField,
|
||||
@@ -166,6 +179,7 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins
|
||||
ConfiguredFields: configuredFields,
|
||||
Interval: interval,
|
||||
IncludeFrozen: includeFrozen,
|
||||
ClusterInfo: clusterInfo,
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package elasticsearch
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
@@ -18,8 +20,26 @@ type datasourceInfo struct {
|
||||
Interval string `json:"interval"`
|
||||
}
|
||||
|
||||
// mockElasticsearchServer creates a test HTTP server that mocks Elasticsearch cluster info endpoint
|
||||
func mockElasticsearchServer() *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// Return a mock Elasticsearch cluster info response
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"version": map[string]interface{}{
|
||||
"build_flavor": "serverless",
|
||||
"number": "8.0.0",
|
||||
},
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
func TestNewInstanceSettings(t *testing.T) {
|
||||
t.Run("fields exist", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: 5,
|
||||
@@ -28,6 +48,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -37,6 +58,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
|
||||
t.Run("timeField", func(t *testing.T) {
|
||||
t.Run("is nil", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
MaxConcurrentShardRequests: 5,
|
||||
Interval: "Daily",
|
||||
@@ -46,6 +70,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -54,6 +79,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("is empty", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
MaxConcurrentShardRequests: 5,
|
||||
Interval: "Daily",
|
||||
@@ -64,6 +92,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -74,6 +103,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
|
||||
t.Run("maxConcurrentShardRequests", func(t *testing.T) {
|
||||
t.Run("no maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
}
|
||||
@@ -81,6 +113,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -90,6 +123,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("string maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: "10",
|
||||
@@ -98,6 +134,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -107,6 +144,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("number maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: 10,
|
||||
@@ -115,6 +155,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -124,6 +165,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("zero maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: 0,
|
||||
@@ -132,6 +176,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -141,6 +186,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("negative maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: -10,
|
||||
@@ -149,6 +197,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -158,6 +207,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("float maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: 10.5,
|
||||
@@ -166,6 +218,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -175,6 +228,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("invalid maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: "invalid",
|
||||
@@ -183,6 +239,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
|
||||
Message: "Health check failed: Failed to get data source info",
|
||||
}, nil
|
||||
}
|
||||
|
||||
healthStatusUrl, err := url.Parse(ds.URL)
|
||||
if err != nil {
|
||||
logger.Error("Failed to parse data source URL", "error", err)
|
||||
@@ -38,6 +37,14 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
|
||||
}, nil
|
||||
}
|
||||
|
||||
// If the cluster is serverless, return a healthy result
|
||||
if ds.ClusterInfo.IsServerless() {
|
||||
return &backend.CheckHealthResult{
|
||||
Status: backend.HealthStatusOk,
|
||||
Message: "Elasticsearch Serverless data source is healthy.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// check that ES is healthy
|
||||
healthStatusUrl.Path = path.Join(healthStatusUrl.Path, "_cluster/health")
|
||||
healthStatusUrl.RawQuery = "wait_for_status=yellow"
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
// metricsResponseProcessor handles processing of metrics query responses
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user