Compare commits
| 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 |
@@ -543,6 +543,7 @@ i18next.config.ts @grafana/grafana-frontend-platform
|
|||||||
/packages/grafana-data/tsconfig.json @grafana/grafana-frontend-platform
|
/packages/grafana-data/tsconfig.json @grafana/grafana-frontend-platform
|
||||||
/packages/grafana-data/test/ @grafana/grafana-frontend-platform
|
/packages/grafana-data/test/ @grafana/grafana-frontend-platform
|
||||||
/packages/grafana-data/typings/ @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/**/*logs* @grafana/observability-logs
|
||||||
/packages/grafana-data/src/context/plugins/ @grafana/plugins-platform-frontend
|
/packages/grafana-data/src/context/plugins/ @grafana/plugins-platform-frontend
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ linters:
|
|||||||
- '**/pkg/tsdb/zipkin/**/*'
|
- '**/pkg/tsdb/zipkin/**/*'
|
||||||
- '**/pkg/tsdb/jaeger/*'
|
- '**/pkg/tsdb/jaeger/*'
|
||||||
- '**/pkg/tsdb/jaeger/**/*'
|
- '**/pkg/tsdb/jaeger/**/*'
|
||||||
|
- '**/pkg/tsdb/elasticsearch/*'
|
||||||
|
- '**/pkg/tsdb/elasticsearch/**/*'
|
||||||
deny:
|
deny:
|
||||||
- pkg: github.com/grafana/grafana/pkg/api
|
- pkg: github.com/grafana/grafana/pkg/api
|
||||||
desc: Core plugins are not allowed to depend on Grafana core packages
|
desc: Core plugins are not allowed to depend on Grafana core packages
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ type check struct {
|
|||||||
PluginStore pluginstore.Store
|
PluginStore pluginstore.Store
|
||||||
PluginContextProvider PluginContextProvider
|
PluginContextProvider PluginContextProvider
|
||||||
PluginClient plugins.Client
|
PluginClient plugins.Client
|
||||||
PluginRepo repo.Service
|
PluginRepo checks.PluginInfoGetter
|
||||||
GrafanaVersion string
|
GrafanaVersion string
|
||||||
pluginCanBeInstalledCache map[string]bool
|
pluginCanBeInstalledCache map[string]bool
|
||||||
pluginExistsCacheMu sync.RWMutex
|
pluginExistsCacheMu sync.RWMutex
|
||||||
@@ -39,7 +39,7 @@ func New(
|
|||||||
pluginStore pluginstore.Store,
|
pluginStore pluginstore.Store,
|
||||||
pluginContextProvider PluginContextProvider,
|
pluginContextProvider PluginContextProvider,
|
||||||
pluginClient plugins.Client,
|
pluginClient plugins.Client,
|
||||||
pluginRepo repo.Service,
|
pluginRepo checks.PluginInfoGetter,
|
||||||
grafanaVersion string,
|
grafanaVersion string,
|
||||||
) checks.Check {
|
) checks.Check {
|
||||||
return &check{
|
return &check{
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
|
|
||||||
type missingPluginStep struct {
|
type missingPluginStep struct {
|
||||||
PluginStore pluginstore.Store
|
PluginStore pluginstore.Store
|
||||||
PluginRepo repo.Service
|
PluginRepo checks.PluginInfoGetter
|
||||||
GrafanaVersion string
|
GrafanaVersion string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana-app-sdk/logging"
|
"github.com/grafana/grafana-app-sdk/logging"
|
||||||
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
|
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
|
// 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 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)
|
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(
|
func New(
|
||||||
pluginStore pluginstore.Store,
|
pluginStore pluginstore.Store,
|
||||||
pluginRepo repo.Service,
|
pluginRepo checks.PluginInfoGetter,
|
||||||
updateChecker pluginchecker.PluginUpdateChecker,
|
updateChecker pluginchecker.PluginUpdateChecker,
|
||||||
pluginErrorResolver plugins.ErrorResolver,
|
pluginErrorResolver plugins.ErrorResolver,
|
||||||
grafanaVersion string,
|
grafanaVersion string,
|
||||||
@@ -33,7 +33,7 @@ func New(
|
|||||||
|
|
||||||
type check struct {
|
type check struct {
|
||||||
PluginStore pluginstore.Store
|
PluginStore pluginstore.Store
|
||||||
PluginRepo repo.Service
|
PluginRepo checks.PluginInfoGetter
|
||||||
updateChecker pluginchecker.PluginUpdateChecker
|
updateChecker pluginchecker.PluginUpdateChecker
|
||||||
pluginErrorResolver plugins.ErrorResolver
|
pluginErrorResolver plugins.ErrorResolver
|
||||||
GrafanaVersion string
|
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:
|
1. Set the data source's basic configuration options:
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| ----------- | ------------------------------------------------------------------------ |
|
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **Name** | Sets the name you use to refer to the data source in panels and queries. |
|
| **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. |
|
| **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
|
### Provision the data source
|
||||||
|
|
||||||
@@ -129,6 +130,7 @@ datasources:
|
|||||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||||
authenticationType: jwt
|
authenticationType: jwt
|
||||||
defaultProject: my-project-name
|
defaultProject: my-project-name
|
||||||
|
universeDomain: googleapis.com
|
||||||
secureJsonData:
|
secureJsonData:
|
||||||
privateKey: |
|
privateKey: |
|
||||||
-----BEGIN PRIVATE KEY-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
@@ -152,6 +154,7 @@ datasources:
|
|||||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||||
authenticationType: jwt
|
authenticationType: jwt
|
||||||
defaultProject: my-project-name
|
defaultProject: my-project-name
|
||||||
|
universeDomain: googleapis.com
|
||||||
privateKeyPath: /etc/secrets/gce.pem
|
privateKeyPath: /etc/secrets/gce.pem
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -166,6 +169,7 @@ datasources:
|
|||||||
access: proxy
|
access: proxy
|
||||||
jsonData:
|
jsonData:
|
||||||
authenticationType: gce
|
authenticationType: gce
|
||||||
|
universeDomain: googleapis.com
|
||||||
```
|
```
|
||||||
|
|
||||||
## Import pre-configured dashboards
|
## 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)
|
- [CockroachDB](/grafana/plugins/grafana-cockroachdb-datasource)
|
||||||
- [Databricks](/grafana/plugins/grafana-databricks-datasource)
|
- [Databricks](/grafana/plugins/grafana-databricks-datasource)
|
||||||
- [DataDog](/grafana/plugins/grafana-datadog-datasource)
|
- [DataDog](/grafana/plugins/grafana-datadog-datasource)
|
||||||
|
- [IBM Db2](/grafana/plugins/grafana-ibmdb2-datasource)
|
||||||
- [Drone](/grafana/plugins/grafana-drone-datasource)
|
- [Drone](/grafana/plugins/grafana-drone-datasource)
|
||||||
- [DynamoDB](/grafana/plugins/grafana-dynamodb-datasource/)
|
- [DynamoDB](/grafana/plugins/grafana-dynamodb-datasource/)
|
||||||
- [Dynatrace](/grafana/plugins/grafana-dynatrace-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 |
|
| `reportingRetries` | Enables rendering retries for the reporting feature |
|
||||||
| `externalServiceAccounts` | Automatic service account and token setup for plugins |
|
| `externalServiceAccounts` | Automatic service account and token setup for plugins |
|
||||||
| `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches |
|
| `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches |
|
||||||
|
| `dashboardNewLayouts` | Enables new dashboard layouts |
|
||||||
| `pdfTables` | Enables generating table data as PDF in reporting |
|
| `pdfTables` | Enables generating table data as PDF in reporting |
|
||||||
| `canvasPanelPanZoom` | Allow pan and zoom in canvas panel |
|
| `canvasPanelPanZoom` | Allow pan and zoom in canvas panel |
|
||||||
| `alertingSaveStateCompressed` | Enables the compressed protobuf-based alert state storage. Default is enabled. |
|
| `alertingSaveStateCompressed` | Enables the compressed protobuf-based alert state storage. Default is enabled. |
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ refs:
|
|||||||
|
|
||||||
# Datagrid
|
# 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
|
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.
|
inside a dashboard.
|
||||||
|
|||||||
@@ -3743,46 +3743,21 @@
|
|||||||
"count": 1
|
"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": {
|
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/aggregations.ts": {
|
||||||
"@typescript-eslint/consistent-type-assertions": {
|
"@typescript-eslint/consistent-type-assertions": {
|
||||||
"count": 1
|
"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": {
|
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.tsx": {
|
||||||
"@typescript-eslint/consistent-type-assertions": {
|
"@typescript-eslint/consistent-type-assertions": {
|
||||||
"count": 1
|
"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": {
|
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/aggregations.ts": {
|
||||||
"@typescript-eslint/consistent-type-assertions": {
|
"@typescript-eslint/consistent-type-assertions": {
|
||||||
"count": 1
|
"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": {
|
"public/app/plugins/datasource/elasticsearch/configuration/DataLinks.tsx": {
|
||||||
"no-restricted-syntax": {
|
"no-restricted-syntax": {
|
||||||
"count": 1
|
"count": 1
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ require (
|
|||||||
github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group
|
github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group
|
||||||
github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend
|
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/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/v2 v2.5.7 // @grafana/grafana-search-and-storage
|
||||||
github.com/blevesearch/bleve_index_api v1.2.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 v0.2.2 // @grafana/grafana-backend-group
|
||||||
github.com/blugelabs/bluge_segment_api v0.2.0 // @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
|
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/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bits-and-blooms/bitset v1.22.0 // indirect
|
github.com/bits-and-blooms/bitset v1.22.0 // indirect
|
||||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||||
github.com/blevesearch/geo v0.1.20 // indirect
|
github.com/blevesearch/geo v0.2.4 // indirect
|
||||||
github.com/blevesearch/go-faiss v1.0.25 // indirect
|
github.com/blevesearch/go-faiss v1.0.26 // indirect
|
||||||
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
||||||
github.com/blevesearch/gtreap v0.1.1 // indirect
|
github.com/blevesearch/gtreap v0.1.1 // indirect
|
||||||
github.com/blevesearch/mmap-go v1.0.4 // 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/segment v0.9.1 // indirect
|
||||||
github.com/blevesearch/snowballstem v0.9.0 // indirect
|
github.com/blevesearch/snowballstem v0.9.0 // indirect
|
||||||
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
|
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
|
||||||
github.com/blevesearch/vellum v1.1.0 // indirect
|
github.com/blevesearch/vellum v1.1.0 // indirect
|
||||||
github.com/blevesearch/zapx/v11 v11.4.1 // indirect
|
github.com/blevesearch/zapx/v11 v11.4.2 // indirect
|
||||||
github.com/blevesearch/zapx/v12 v12.4.1 // indirect
|
github.com/blevesearch/zapx/v12 v12.4.2 // indirect
|
||||||
github.com/blevesearch/zapx/v13 v13.4.1 // indirect
|
github.com/blevesearch/zapx/v13 v13.4.2 // indirect
|
||||||
github.com/blevesearch/zapx/v14 v14.4.1 // indirect
|
github.com/blevesearch/zapx/v14 v14.4.2 // indirect
|
||||||
github.com/blevesearch/zapx/v15 v15.4.1 // indirect
|
github.com/blevesearch/zapx/v15 v15.4.2 // indirect
|
||||||
github.com/blevesearch/zapx/v16 v16.2.2 // indirect
|
github.com/blevesearch/zapx/v16 v16.2.8 // indirect
|
||||||
github.com/bluele/gcache v0.0.2 // indirect
|
github.com/bluele/gcache v0.0.2 // indirect
|
||||||
github.com/blugelabs/ice v1.0.0 // indirect
|
github.com/blugelabs/ice v1.0.0 // indirect
|
||||||
github.com/blugelabs/ice/v2 v2.0.1 // 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-jwt/jwt/v5 v5.3.0 // indirect
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
github.com/golang-sql/sqlexp v0.1.0 // 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/gomodule/redigo v1.8.9 // indirect
|
||||||
github.com/google/btree v1.1.3 // indirect
|
github.com/google/btree v1.1.3 // indirect
|
||||||
github.com/google/cel-go v0.26.1 // indirect
|
github.com/google/cel-go v0.26.1 // indirect
|
||||||
|
|||||||
@@ -931,14 +931,14 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn
|
|||||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
github.com/blang/semver 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 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
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.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8=
|
||||||
github.com/blevesearch/bleve/v2 v2.5.0/go.mod h1:PcJzTPnEynO15dCf9isxOga7YFRa/cMSsbnRwnszXUk=
|
github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA=
|
||||||
github.com/blevesearch/bleve_index_api v1.2.7 h1:c8r9vmbaYQroAMSGag7zq5gEVPiuXrUQDqfnj7uYZSY=
|
github.com/blevesearch/bleve_index_api v1.3.0 h1:DsMpWVjFNlBw9/6pyWf59XoqcAkhHj3H0UWiQsavb6E=
|
||||||
github.com/blevesearch/bleve_index_api v1.2.7/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
|
github.com/blevesearch/bleve_index_api v1.3.0/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
|
||||||
github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
|
github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
|
||||||
github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=
|
github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
|
||||||
github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U=
|
github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI=
|
||||||
github.com/blevesearch/go-faiss v1.0.25/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
|
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 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
|
||||||
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
|
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
|
||||||
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
|
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.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 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
|
||||||
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
|
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.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY=
|
||||||
github.com/blevesearch/scorch_segment_api/v2 v2.3.9/go.mod h1:IrzspZlVjhf4X29oJiEhBxEteTqOY9RlYlk1lCmYHr4=
|
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.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 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
|
||||||
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
|
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.0.7/go.mod h1:doBZpmRhwTsASB4QdUZANlJvqVAUdUyX0ZK7QJCTeBE=
|
||||||
github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w=
|
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/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.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs=
|
||||||
github.com/blevesearch/zapx/v11 v11.4.1/go.mod h1:qNOGxIqdPC1MXauJCD9HBG487PxviTUUbmChFOAosGs=
|
github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
|
||||||
github.com/blevesearch/zapx/v12 v12.4.1 h1:K77bhypII60a4v8mwvav7r4IxWA8qxhNjgF9xGdb9eQ=
|
github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE=
|
||||||
github.com/blevesearch/zapx/v12 v12.4.1/go.mod h1:QRPrlPOzAxBNMI0MkgdD+xsTqx65zbuPr3Ko4Re49II=
|
github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
|
||||||
github.com/blevesearch/zapx/v13 v13.4.1 h1:EnkEMZFUK0lsW/jOJJF2xOcp+W8TjEsyeN5BeAZEYYE=
|
github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks=
|
||||||
github.com/blevesearch/zapx/v13 v13.4.1/go.mod h1:e6duBMlCvgbH9rkzNMnUa9hRI9F7ri2BRcHfphcmGn8=
|
github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
|
||||||
github.com/blevesearch/zapx/v14 v14.4.1 h1:G47kGCshknBZzZAtjcnIAMn3oNx8XBLxp8DMq18ogyE=
|
github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0=
|
||||||
github.com/blevesearch/zapx/v14 v14.4.1/go.mod h1:O7sDxiaL2r2PnCXbhh1Bvm7b4sP+jp4unE9DDPWGoms=
|
github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
|
||||||
github.com/blevesearch/zapx/v15 v15.4.1 h1:B5IoTMUCEzFdc9FSQbhVOxAY+BO17c05866fNruiI7g=
|
github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k=
|
||||||
github.com/blevesearch/zapx/v15 v15.4.1/go.mod h1:b/MreHjYeQoLjyY2+UaM0hGZZUajEbE0xhnr1A2/Q6Y=
|
github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
|
||||||
github.com/blevesearch/zapx/v16 v16.2.2 h1:MifKJVRTEhMTgSlle2bDRTb39BGc9jXFRLPZc6r0Rzk=
|
github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI=
|
||||||
github.com/blevesearch/zapx/v16 v16.2.2/go.mod h1:B9Pk4G1CqtErgQV9DyCSA9Lb7WZe4olYfGw7fVDZ4sk=
|
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 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
|
||||||
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
|
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
|
||||||
github.com/blugelabs/bluge v0.2.2 h1:gat8CqE6P6tOgeX30XGLOVNTC26cpM2RWVcreXWtYcM=
|
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 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
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/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 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.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||||
github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
|
github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
|
||||||
|
|||||||
+27
-2
@@ -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/benbjohnson/immutable v0.4.0/go.mod h1:iAr8OjJGLnLmVUr9MZ/rz4PWUy6Ouc2JLYuMArmvAJM=
|
||||||
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
|
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/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 h1:kDy+zgJFJJoJYBvdfBSiZYBbdsUL0XcjHYWezpQBGPA=
|
||||||
github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:9eJDeqxJ3E7WnLebQUlPD7ZjSce7AnDb9vjGmMCbD0A=
|
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 h1:iAtV2Cu5s0GD1lwUiekkFHe2gTMCCNVj2foPclDLIFI=
|
||||||
github.com/blevesearch/goleveldb v1.0.1/go.mod h1:WrU8ltZbIp0wAoig/MHbrPCXSOLpe79nz5lv5nqfYrQ=
|
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 h1:cDYjn/NCH+wwt2UdehaLpr2e4BwLIjN4V/TdLsL+B5A=
|
||||||
github.com/blevesearch/snowball v0.6.1/go.mod h1:ZF0IBg5vgpeoUhnMza2v0A/z8m1cWPlwhke08LpNusg=
|
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 h1:CYzVPaScODMvgE9o+kf6D4RJ/VRomyi9uHF+PtB+Afc=
|
||||||
github.com/blevesearch/stempel v0.2.0/go.mod h1:wjeTHqQv+nQdbPuJ/YcvOjTInA2EIc6Ks1FoSUzSLvc=
|
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 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
|
||||||
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
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=
|
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.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 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.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-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/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=
|
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 h1:sfe532Ipn7GX0V6mHdynBk393rDmqgI0QmjLK7ct7TU=
|
||||||
github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a/go.mod h1:dNKs71rs2VJGBAmttu7fouEsRQlRjxy0p1Sx+T5wbpY=
|
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/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/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 h1:z1n1AOHVVydOOVuyphbOKyR4NICDQFiJMn1IK5hVQ5Y=
|
||||||
github.com/jsternberg/zap-logfmt v1.3.0/go.mod h1:N3DENp9WNmCZxvkBD/eReWwz1149BK6jEN9cQ4fNwZE=
|
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.
|
// Decoupled plugins run their own tests so ignoring them here.
|
||||||
'<rootDir>/public/app/plugins/datasource/azuremonitor',
|
'<rootDir>/public/app/plugins/datasource/azuremonitor',
|
||||||
'<rootDir>/public/app/plugins/datasource/cloud-monitoring',
|
'<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-postgresql-datasource',
|
||||||
'<rootDir>/public/app/plugins/datasource/grafana-pyroscope-datasource',
|
'<rootDir>/public/app/plugins/datasource/grafana-pyroscope-datasource',
|
||||||
'<rootDir>/public/app/plugins/datasource/grafana-testdata-datasource',
|
'<rootDir>/public/app/plugins/datasource/grafana-testdata-datasource',
|
||||||
|
|||||||
+2
-2
@@ -293,8 +293,8 @@
|
|||||||
"@grafana/plugin-ui": "^0.11.1",
|
"@grafana/plugin-ui": "^0.11.1",
|
||||||
"@grafana/prometheus": "workspace:*",
|
"@grafana/prometheus": "workspace:*",
|
||||||
"@grafana/runtime": "workspace:*",
|
"@grafana/runtime": "workspace:*",
|
||||||
"@grafana/scenes": "v6.52.1",
|
"@grafana/scenes": "6.52.2",
|
||||||
"@grafana/scenes-react": "v6.52.1",
|
"@grafana/scenes-react": "6.52.2",
|
||||||
"@grafana/schema": "workspace:*",
|
"@grafana/schema": "workspace:*",
|
||||||
"@grafana/sql": "workspace:*",
|
"@grafana/sql": "workspace:*",
|
||||||
"@grafana/ui": "workspace:*",
|
"@grafana/ui": "workspace:*",
|
||||||
|
|||||||
@@ -35,6 +35,14 @@
|
|||||||
},
|
},
|
||||||
"./test": {
|
"./test": {
|
||||||
"@grafana-app/source": "./test/index.ts"
|
"@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": {
|
"publishConfig": {
|
||||||
@@ -52,7 +60,7 @@
|
|||||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
|
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
|
||||||
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
|
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
|
||||||
"postpack": "mv package.json.bak package.json",
|
"postpack": "mv package.json.bak package.json",
|
||||||
"themes-schema": "tsx ./src/themes/scripts/generateSchema.ts"
|
"themes-schema": "tsx ./scripts/generateSchema.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "7.0.1",
|
"@braintree/sanitize-url": "7.0.1",
|
||||||
@@ -102,6 +110,7 @@
|
|||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"rimraf": "6.0.1",
|
"rimraf": "6.0.1",
|
||||||
"rollup": "^4.22.4",
|
"rollup": "^4.22.4",
|
||||||
|
"rollup-plugin-copy": "3.5.0",
|
||||||
"rollup-plugin-esbuild": "6.2.1",
|
"rollup-plugin-esbuild": "6.2.1",
|
||||||
"rollup-plugin-node-externals": "^8.0.0",
|
"rollup-plugin-node-externals": "^8.0.0",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
|
|||||||
@@ -1,21 +1,40 @@
|
|||||||
import json from '@rollup/plugin-json';
|
import json from '@rollup/plugin-json';
|
||||||
import { createRequire } from 'node:module';
|
import { createRequire } from 'node:module';
|
||||||
|
import copy from 'rollup-plugin-copy';
|
||||||
|
|
||||||
import { entryPoint, plugins, esmOutput, cjsOutput } from '../rollup.config.parts';
|
import { entryPoint, plugins, esmOutput, cjsOutput } from '../rollup.config.parts';
|
||||||
|
|
||||||
const rq = createRequire(import.meta.url);
|
const rq = createRequire(import.meta.url);
|
||||||
const pkg = rq('./package.json');
|
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 [
|
export default [
|
||||||
{
|
{
|
||||||
input: entryPoint,
|
input: entryPoint,
|
||||||
plugins: [...plugins, json()],
|
plugins: grafanaDataPlugins,
|
||||||
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
|
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
|
||||||
treeshake: false,
|
treeshake: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: 'src/unstable.ts',
|
input: 'src/unstable.ts',
|
||||||
plugins: [...plugins, json()],
|
plugins: grafanaDataPlugins,
|
||||||
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
|
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
|
||||||
treeshake: false,
|
treeshake: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
DataLinkConfigOrigin,
|
||||||
SupportedTransformationType,
|
SupportedTransformationType,
|
||||||
type InternalDataLink,
|
type InternalDataLink,
|
||||||
type LinkTarget,
|
|
||||||
type LinkModel,
|
type LinkModel,
|
||||||
type LinkModelSupplier,
|
type LinkModelSupplier,
|
||||||
VariableOrigin,
|
VariableOrigin,
|
||||||
@@ -852,6 +851,7 @@ export {
|
|||||||
VariableSuggestionsScope,
|
VariableSuggestionsScope,
|
||||||
OneClickMode,
|
OneClickMode,
|
||||||
} from './types/dataLink';
|
} from './types/dataLink';
|
||||||
|
export { type LinkTarget } from './types/linkTarget';
|
||||||
export {
|
export {
|
||||||
type Action,
|
type Action,
|
||||||
type ActionModel,
|
type ActionModel,
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ export { DataTransformerID } from '../transformations/transformers/ids';
|
|||||||
|
|
||||||
export { mergeTransformer } from '../transformations/transformers/merge';
|
export { mergeTransformer } from '../transformations/transformers/merge';
|
||||||
export { getThemeById } from '../themes/registry';
|
export { getThemeById } from '../themes/registry';
|
||||||
export * as experimentalThemeDefinitions from '../themes/themeDefinitions';
|
|
||||||
export { GrafanaEdition } from '../types/config';
|
export { GrafanaEdition } from '../types/config';
|
||||||
export { SIPrefix } from '../valueFormats/symbolFormatters';
|
export { SIPrefix } from '../valueFormats/symbolFormatters';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
import { Registry, RegistryItem } from '../utils/Registry';
|
import { Registry, RegistryItem } from '../utils/Registry';
|
||||||
|
|
||||||
import { createTheme, NewThemeOptionsSchema } from './createTheme';
|
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';
|
import { GrafanaTheme2 } from './types';
|
||||||
|
|
||||||
export interface ThemeRegistryItem extends RegistryItem {
|
export interface ThemeRegistryItem extends RegistryItem {
|
||||||
@@ -9,6 +20,21 @@ export interface ThemeRegistryItem extends RegistryItem {
|
|||||||
build: () => GrafanaTheme2;
|
build: () => GrafanaTheme2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extraThemes: { [key: string]: unknown } = {
|
||||||
|
aubergine,
|
||||||
|
debug,
|
||||||
|
desertbloom,
|
||||||
|
gildedgrove,
|
||||||
|
gloom,
|
||||||
|
mars,
|
||||||
|
matrix,
|
||||||
|
sapphiredusk,
|
||||||
|
synthwave,
|
||||||
|
tron,
|
||||||
|
victorian,
|
||||||
|
zen,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
* Only for internal use, never use this from a plugin
|
* 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 { ScopedVars } from './ScopedVars';
|
||||||
import { ExploreCorrelationHelperData, ExplorePanelsState } from './explore';
|
import { ExploreCorrelationHelperData, ExplorePanelsState } from './explore';
|
||||||
|
import { LinkTarget } from './linkTarget';
|
||||||
import { InterpolateFunction } from './panel';
|
import { InterpolateFunction } from './panel';
|
||||||
import { DataQuery } from './query';
|
import { DataQuery } from './query';
|
||||||
import { TimeRange } from './time';
|
import { TimeRange } from './time';
|
||||||
@@ -88,8 +89,6 @@ export interface InternalDataLink<T extends DataQuery = any> {
|
|||||||
range?: TimeRange;
|
range?: TimeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LinkTarget = '_blank' | '_self' | undefined;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processed Link Model. The values are ready to use
|
* Processed Link Model. The values are ready to use
|
||||||
*/
|
*/
|
||||||
|
|||||||
+9
-1
@@ -356,7 +356,7 @@ export interface FeatureToggles {
|
|||||||
*/
|
*/
|
||||||
dashboardScene?: boolean;
|
dashboardScene?: boolean;
|
||||||
/**
|
/**
|
||||||
* Enables experimental new dashboard layouts
|
* Enables new dashboard layouts
|
||||||
*/
|
*/
|
||||||
dashboardNewLayouts?: boolean;
|
dashboardNewLayouts?: boolean;
|
||||||
/**
|
/**
|
||||||
@@ -531,6 +531,10 @@ export interface FeatureToggles {
|
|||||||
*/
|
*/
|
||||||
alertingListViewV2?: boolean;
|
alertingListViewV2?: boolean;
|
||||||
/**
|
/**
|
||||||
|
* Enables the new Alerting navigation structure with improved menu grouping
|
||||||
|
*/
|
||||||
|
alertingNavigationV2?: boolean;
|
||||||
|
/**
|
||||||
* Enables saved searches for alert rules list
|
* Enables saved searches for alert rules list
|
||||||
*/
|
*/
|
||||||
alertingSavedSearches?: boolean;
|
alertingSavedSearches?: boolean;
|
||||||
@@ -1251,4 +1255,8 @@ export interface FeatureToggles {
|
|||||||
* Enables profiles exemplars support in profiles drilldown
|
* Enables profiles exemplars support in profiles drilldown
|
||||||
*/
|
*/
|
||||||
profilesExemplars?: boolean;
|
profilesExemplars?: boolean;
|
||||||
|
/**
|
||||||
|
* Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods
|
||||||
|
*/
|
||||||
|
alertingSyncDispatchTimer?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { ComponentType } from 'react';
|
||||||
|
|
||||||
import { LinkTarget } from './dataLink';
|
|
||||||
import { IconName } from './icon';
|
import { IconName } from './icon';
|
||||||
|
import { LinkTarget } from './linkTarget';
|
||||||
|
|
||||||
export interface NavLinkDTO {
|
export interface NavLinkDTO {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { DataFrame } from './dataFrame';
|
|||||||
import { DataQueryError, DataQueryRequest, DataQueryTimings } from './datasource';
|
import { DataQueryError, DataQueryRequest, DataQueryTimings } from './datasource';
|
||||||
import { FieldConfigSource } from './fieldOverrides';
|
import { FieldConfigSource } from './fieldOverrides';
|
||||||
import { IconName } from './icon';
|
import { IconName } from './icon';
|
||||||
|
import { LinkTarget } from './linkTarget';
|
||||||
import { OptionEditorConfig } from './options';
|
import { OptionEditorConfig } from './options';
|
||||||
import { PluginMeta } from './plugin';
|
import { PluginMeta } from './plugin';
|
||||||
import { AbsoluteTimeRange, TimeRange, TimeZone } from './time';
|
import { AbsoluteTimeRange, TimeRange, TimeZone } from './time';
|
||||||
@@ -191,6 +192,7 @@ export interface PanelMenuItem {
|
|||||||
onClick?: (event: React.MouseEvent) => void;
|
onClick?: (event: React.MouseEvent) => void;
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
href?: string;
|
href?: string;
|
||||||
|
target?: LinkTarget;
|
||||||
subMenu?: PanelMenuItem[];
|
subMenu?: PanelMenuItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,4 +9,4 @@
|
|||||||
* and be subject to the standard policies
|
* and be subject to the standard policies
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { default as themeJsonSchema } from './themes/schema.generated.json';
|
export {};
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"emitDeclarationOnly": true,
|
"emitDeclarationOnly": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"rootDirs": ["."],
|
"rootDirs": ["."],
|
||||||
"moduleResolution": "bundler"
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true
|
||||||
},
|
},
|
||||||
"exclude": ["dist/**/*"],
|
"exclude": ["dist/**/*"],
|
||||||
"include": [
|
"include": [
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
"d3": "^7.8.5",
|
"d3": "^7.8.5",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
|
"react-table": "^7.8.0",
|
||||||
"react-use": "17.6.0",
|
"react-use": "17.6.0",
|
||||||
"react-virtualized-auto-sizer": "1.0.26",
|
"react-virtualized-auto-sizer": "1.0.26",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
@@ -81,6 +82,7 @@
|
|||||||
"@types/lodash": "4.17.20",
|
"@types/lodash": "4.17.20",
|
||||||
"@types/node": "24.10.1",
|
"@types/node": "24.10.1",
|
||||||
"@types/react": "18.3.18",
|
"@types/react": "18.3.18",
|
||||||
|
"@types/react-table": "^7.7.20",
|
||||||
"@types/react-virtualized-auto-sizer": "1.0.8",
|
"@types/react-virtualized-auto-sizer": "1.0.8",
|
||||||
"@types/tinycolor2": "1.4.6",
|
"@types/tinycolor2": "1.4.6",
|
||||||
"babel-jest": "29.7.0",
|
"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
@@ -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,
|
rangeMax: 1,
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
colorScheme: ColorScheme.PackageBased,
|
colorScheme: ColorScheme.PackageBased,
|
||||||
selectedView: SelectedView.Both,
|
selectedView: SelectedView.Multi,
|
||||||
search: '',
|
search: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,10 +43,13 @@ describe('FlameGraph', () => {
|
|||||||
setRangeMax={setRangeMax}
|
setRangeMax={setRangeMax}
|
||||||
onItemFocused={onItemFocused}
|
onItemFocused={onItemFocused}
|
||||||
textAlign={'left'}
|
textAlign={'left'}
|
||||||
|
onTextAlignChange={jest.fn()}
|
||||||
onSandwich={onSandwich}
|
onSandwich={onSandwich}
|
||||||
onFocusPillClick={onFocusPillClick}
|
onFocusPillClick={onFocusPillClick}
|
||||||
onSandwichPillClick={onSandwichPillClick}
|
onSandwichPillClick={onSandwichPillClick}
|
||||||
colorScheme={ColorScheme.ValueBased}
|
colorScheme={ColorScheme.ValueBased}
|
||||||
|
onColorSchemeChange={jest.fn()}
|
||||||
|
isDiffMode={false}
|
||||||
selectedView={SelectedView.FlameGraph}
|
selectedView={SelectedView.FlameGraph}
|
||||||
search={''}
|
search={''}
|
||||||
collapsedMap={container.getCollapsedMap()}
|
collapsedMap={container.getCollapsedMap()}
|
||||||
|
|||||||
@@ -19,8 +19,10 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { useEffect, useState } from 'react';
|
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 { PIXELS_PER_LEVEL } from '../constants';
|
||||||
import { ClickedItemData, ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from '../types';
|
import { ClickedItemData, ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from '../types';
|
||||||
|
|
||||||
@@ -39,11 +41,14 @@ type Props = {
|
|||||||
onItemFocused: (data: ClickedItemData) => void;
|
onItemFocused: (data: ClickedItemData) => void;
|
||||||
focusedItemData?: ClickedItemData;
|
focusedItemData?: ClickedItemData;
|
||||||
textAlign: TextAlign;
|
textAlign: TextAlign;
|
||||||
|
onTextAlignChange: (align: TextAlign) => void;
|
||||||
sandwichItem?: string;
|
sandwichItem?: string;
|
||||||
onSandwich: (label: string) => void;
|
onSandwich: (label: string) => void;
|
||||||
onFocusPillClick: () => void;
|
onFocusPillClick: () => void;
|
||||||
onSandwichPillClick: () => void;
|
onSandwichPillClick: () => void;
|
||||||
colorScheme: ColorScheme | ColorSchemeDiff;
|
colorScheme: ColorScheme | ColorSchemeDiff;
|
||||||
|
onColorSchemeChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
|
||||||
|
isDiffMode: boolean;
|
||||||
showFlameGraphOnly?: boolean;
|
showFlameGraphOnly?: boolean;
|
||||||
getExtraContextMenuButtons?: GetExtraContextMenuButtonsFunction;
|
getExtraContextMenuButtons?: GetExtraContextMenuButtonsFunction;
|
||||||
collapsing?: boolean;
|
collapsing?: boolean;
|
||||||
@@ -63,11 +68,14 @@ const FlameGraph = ({
|
|||||||
onItemFocused,
|
onItemFocused,
|
||||||
focusedItemData,
|
focusedItemData,
|
||||||
textAlign,
|
textAlign,
|
||||||
|
onTextAlignChange,
|
||||||
onSandwich,
|
onSandwich,
|
||||||
sandwichItem,
|
sandwichItem,
|
||||||
onFocusPillClick,
|
onFocusPillClick,
|
||||||
onSandwichPillClick,
|
onSandwichPillClick,
|
||||||
colorScheme,
|
colorScheme,
|
||||||
|
onColorSchemeChange,
|
||||||
|
isDiffMode,
|
||||||
showFlameGraphOnly,
|
showFlameGraphOnly,
|
||||||
getExtraContextMenuButtons,
|
getExtraContextMenuButtons,
|
||||||
collapsing,
|
collapsing,
|
||||||
@@ -76,7 +84,7 @@ const FlameGraph = ({
|
|||||||
collapsedMap,
|
collapsedMap,
|
||||||
setCollapsedMap,
|
setCollapsedMap,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const styles = getStyles();
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const [levels, setLevels] = useState<LevelItem[][]>();
|
const [levels, setLevels] = useState<LevelItem[][]>();
|
||||||
const [levelsCallers, setLevelsCallers] = 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 (
|
return (
|
||||||
<div className={styles.graph}>
|
<div className={styles.graph}>
|
||||||
<FlameGraphMetadata
|
<div className={styles.toolbar}>
|
||||||
data={data}
|
<FlameGraphMetadata
|
||||||
focusedItem={focusedItemData}
|
data={data}
|
||||||
sandwichedLabel={sandwichItem}
|
focusedItem={focusedItemData}
|
||||||
totalTicks={totalViewTicks}
|
sandwichedLabel={sandwichItem}
|
||||||
onFocusPillClick={onFocusPillClick}
|
totalTicks={totalViewTicks}
|
||||||
onSandwichPillClick={onSandwichPillClick}
|
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}
|
{canvas}
|
||||||
</div>
|
</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({
|
graph: css({
|
||||||
label: 'graph',
|
label: 'graph',
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
flexBasis: '50%',
|
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({
|
sandwichCanvasWrapper: css({
|
||||||
label: 'sandwichCanvasWrapper',
|
label: 'sandwichCanvasWrapper',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,18 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import uFuzzy from '@leeoniya/ufuzzy';
|
import uFuzzy from '@leeoniya/ufuzzy';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useMeasure } from 'react-use';
|
import { useMeasure } from 'react-use';
|
||||||
|
|
||||||
import { DataFrame, GrafanaTheme2, escapeStringForRegex } from '@grafana/data';
|
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { ThemeContext } from '@grafana/ui';
|
import { ThemeContext } from '@grafana/ui';
|
||||||
|
|
||||||
import FlameGraph from './FlameGraph/FlameGraph';
|
import { FlameGraphDataContainer } from './FlameGraph/dataTransform';
|
||||||
import { GetExtraContextMenuButtonsFunction } from './FlameGraph/FlameGraphContextMenu';
|
import { GetExtraContextMenuButtonsFunction } from './FlameGraph/FlameGraphContextMenu';
|
||||||
import { CollapsedMap, FlameGraphDataContainer } from './FlameGraph/dataTransform';
|
|
||||||
import FlameGraphHeader from './FlameGraphHeader';
|
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 { 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';
|
import { getAssistantContextFromDataFrame } from './utils';
|
||||||
|
|
||||||
const ufuzzy = new uFuzzy();
|
const ufuzzy = new uFuzzy();
|
||||||
@@ -104,17 +103,18 @@ const FlameGraphContainer = ({
|
|||||||
getExtraContextMenuButtons,
|
getExtraContextMenuButtons,
|
||||||
showAnalyzeWithAssistant = true,
|
showAnalyzeWithAssistant = true,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [focusedItemData, setFocusedItemData] = useState<ClickedItemData>();
|
// Shared state across all views
|
||||||
|
|
||||||
const [rangeMin, setRangeMin] = useState(0);
|
|
||||||
const [rangeMax, setRangeMax] = useState(1);
|
|
||||||
const [search, setSearch] = useState('');
|
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 [sizeRef, { width: containerWidth }] = useMeasure<HTMLDivElement>();
|
||||||
const [textAlign, setTextAlign] = useState<TextAlign>('left');
|
// Used to trigger reset of pane-specific state (focus, sandwich) when parent reset button is clicked
|
||||||
// This is a label of the item because in sandwich view we group all items by label and present a merged graph
|
const [resetKey, setResetKey] = useState(0);
|
||||||
const [sandwichItem, setSandwichItem] = useState<string>();
|
// Track if we temporarily switched away from Both view due to narrow width
|
||||||
const [collapsedMap, setCollapsedMap] = useState(new CollapsedMap());
|
const [viewBeforeNarrow, setViewBeforeNarrow] = useState<SelectedView | null>(null);
|
||||||
|
|
||||||
const theme = useMemo(() => getTheme(), [getTheme]);
|
const theme = useMemo(() => getTheme(), [getTheme]);
|
||||||
const dataContainer = useMemo((): FlameGraphDataContainer | undefined => {
|
const dataContainer = useMemo((): FlameGraphDataContainer | undefined => {
|
||||||
@@ -122,157 +122,220 @@ const FlameGraphContainer = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = new FlameGraphDataContainer(data, { collapsing: !disableCollapsing }, theme);
|
return new FlameGraphDataContainer(data, { collapsing: !disableCollapsing }, theme);
|
||||||
setCollapsedMap(container.getCollapsedMap());
|
|
||||||
return container;
|
|
||||||
}, [data, theme, disableCollapsing]);
|
}, [data, theme, disableCollapsing]);
|
||||||
const [colorScheme, setColorScheme] = useColorScheme(dataContainer);
|
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
const matchedLabels = useLabelSearch(search, dataContainer);
|
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(() => {
|
useEffect(() => {
|
||||||
if (
|
if (containerWidth === 0) {
|
||||||
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();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataContainer && focusedItemData) {
|
const isNarrow = containerWidth < MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH && !vertical;
|
||||||
const item = dataContainer.getNodesWithLabel(focusedItemData.label)?.[0];
|
|
||||||
|
|
||||||
if (item) {
|
if (isNarrow && selectedView === SelectedView.Multi) {
|
||||||
setFocusedItemData({ ...focusedItemData, item });
|
// Going narrow: save current view and switch to FlameGraph
|
||||||
|
setViewBeforeNarrow(SelectedView.Multi);
|
||||||
const levels = dataContainer.getLevels();
|
setSelectedView(SelectedView.FlameGraph);
|
||||||
const totalViewTicks = levels.length ? levels[0][0].value : 0;
|
} else if (!isNarrow && viewBeforeNarrow !== null) {
|
||||||
setRangeMin(item.start / totalViewTicks);
|
// Going wide again: restore the previous view
|
||||||
setRangeMax((item.start + item.value) / totalViewTicks);
|
setSelectedView(viewBeforeNarrow);
|
||||||
} else {
|
setViewBeforeNarrow(null);
|
||||||
setFocusedItemData({
|
|
||||||
...focusedItemData,
|
|
||||||
item: {
|
|
||||||
start: 0,
|
|
||||||
value: 0,
|
|
||||||
itemIndexes: [],
|
|
||||||
children: [],
|
|
||||||
level: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setRangeMin(0);
|
|
||||||
setRangeMax(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [dataContainer, keepFocusOnDataChange]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [containerWidth, vertical, selectedView, viewBeforeNarrow]);
|
||||||
|
|
||||||
const onSymbolClick = useCallback(
|
|
||||||
(symbol: string) => {
|
|
||||||
const anchored = `^${escapeStringForRegex(symbol)}$`;
|
|
||||||
|
|
||||||
if (search === anchored) {
|
|
||||||
setSearch('');
|
|
||||||
} else {
|
|
||||||
onTableSymbolClick?.(symbol);
|
|
||||||
setSearch(anchored);
|
|
||||||
resetFocus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[setSearch, resetFocus, onTableSymbolClick, search]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!dataContainer) {
|
if (!dataContainer) {
|
||||||
return null;
|
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;
|
let body;
|
||||||
if (showFlameGraphOnly || selectedView === SelectedView.FlameGraph) {
|
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) {
|
} else if (selectedView === SelectedView.TopTable) {
|
||||||
body = <div className={styles.tableContainer}>{table}</div>;
|
body = (
|
||||||
} else if (selectedView === SelectedView.Both) {
|
<FlameGraphPane
|
||||||
if (vertical) {
|
paneView={PaneView.TopTable}
|
||||||
body = (
|
dataContainer={dataContainer}
|
||||||
<div>
|
search={search}
|
||||||
<div className={styles.verticalGraphContainer}>{flameGraph}</div>
|
matchedLabels={matchedLabels}
|
||||||
<div className={styles.verticalTableContainer}>{table}</div>
|
onTableSymbolClick={onTableSymbolClick}
|
||||||
</div>
|
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 {
|
} else {
|
||||||
|
// Single view mode
|
||||||
body = (
|
body = (
|
||||||
<div className={styles.horizontalContainer}>
|
<div className={styles.singlePaneContainer}>
|
||||||
<div className={styles.horizontalTableContainer}>{table}</div>
|
<FlameGraphPane
|
||||||
<div className={styles.horizontalGraphContainer}>{flameGraph}</div>
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -292,25 +355,24 @@ const FlameGraphContainer = ({
|
|||||||
setSelectedView(view);
|
setSelectedView(view);
|
||||||
onViewSelected?.(view);
|
onViewSelected?.(view);
|
||||||
}}
|
}}
|
||||||
|
viewMode={viewMode}
|
||||||
|
setViewMode={setViewMode}
|
||||||
|
leftPaneView={leftPaneView}
|
||||||
|
setLeftPaneView={setLeftPaneView}
|
||||||
|
rightPaneView={rightPaneView}
|
||||||
|
setRightPaneView={setRightPaneView}
|
||||||
|
singleView={singleView}
|
||||||
|
setSingleView={setSingleView}
|
||||||
containerWidth={containerWidth}
|
containerWidth={containerWidth}
|
||||||
onReset={() => {
|
onReset={() => {
|
||||||
resetFocus();
|
// Reset search and pane states when user clicks reset button
|
||||||
resetSandwich();
|
setSearch('');
|
||||||
|
setResetKey((k) => k + 1);
|
||||||
}}
|
}}
|
||||||
textAlign={textAlign}
|
showResetButton={Boolean(search)}
|
||||||
onTextAlignChange={(align) => {
|
|
||||||
setTextAlign(align);
|
|
||||||
onTextAlignSelected?.(align);
|
|
||||||
}}
|
|
||||||
showResetButton={Boolean(focusedItemData || sandwichItem)}
|
|
||||||
colorScheme={colorScheme}
|
|
||||||
onColorSchemeChange={setColorScheme}
|
|
||||||
stickyHeader={Boolean(stickyHeader)}
|
stickyHeader={Boolean(stickyHeader)}
|
||||||
extraHeaderElements={extraHeaderElements}
|
extraHeaderElements={extraHeaderElements}
|
||||||
vertical={vertical}
|
vertical={vertical}
|
||||||
isDiffMode={dataContainer.isDiffFlamegraph()}
|
|
||||||
setCollapsedMap={setCollapsedMap}
|
|
||||||
collapsedMap={collapsedMap}
|
|
||||||
assistantContext={data && showAnalyzeWithAssistant ? getAssistantContextFromDataFrame(data) : undefined}
|
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.
|
* 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,
|
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({
|
horizontalContainer: css({
|
||||||
label: 'horizontalContainer',
|
label: 'horizontalContainer',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -435,20 +479,20 @@ function getStyles(theme: GrafanaTheme2) {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
horizontalGraphContainer: css({
|
horizontalPaneContainer: css({
|
||||||
flexBasis: '50%',
|
label: 'horizontalPaneContainer',
|
||||||
}),
|
|
||||||
|
|
||||||
horizontalTableContainer: css({
|
|
||||||
flexBasis: '50%',
|
flexBasis: '50%',
|
||||||
maxHeight: 800,
|
maxHeight: 800,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
verticalGraphContainer: css({
|
verticalPaneContainer: css({
|
||||||
|
label: 'verticalPaneContainer',
|
||||||
marginBottom: theme.spacing(1),
|
marginBottom: theme.spacing(1),
|
||||||
|
height: 800,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
verticalTableContainer: css({
|
singlePaneContainer: css({
|
||||||
|
label: 'singlePaneContainer',
|
||||||
height: 800,
|
height: 800,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import { render, screen } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { CollapsedMap } from './FlameGraph/dataTransform';
|
|
||||||
import FlameGraphHeader from './FlameGraphHeader';
|
import FlameGraphHeader from './FlameGraphHeader';
|
||||||
import { ColorScheme, SelectedView } from './types';
|
import { PaneView, SelectedView, ViewMode } from './types';
|
||||||
|
|
||||||
jest.mock('@grafana/assistant', () => ({
|
jest.mock('@grafana/assistant', () => ({
|
||||||
useAssistant: jest.fn().mockReturnValue({
|
useAssistant: jest.fn().mockReturnValue({
|
||||||
@@ -20,26 +19,30 @@ describe('FlameGraphHeader', () => {
|
|||||||
function setup(props: Partial<React.ComponentProps<typeof FlameGraphHeader>> = {}) {
|
function setup(props: Partial<React.ComponentProps<typeof FlameGraphHeader>> = {}) {
|
||||||
const setSearch = jest.fn();
|
const setSearch = jest.fn();
|
||||||
const setSelectedView = 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 onReset = jest.fn();
|
||||||
const onSchemeChange = jest.fn();
|
|
||||||
|
|
||||||
const renderResult = render(
|
const renderResult = render(
|
||||||
<FlameGraphHeader
|
<FlameGraphHeader
|
||||||
search={''}
|
search={''}
|
||||||
setSearch={setSearch}
|
setSearch={setSearch}
|
||||||
selectedView={SelectedView.Both}
|
selectedView={SelectedView.Multi}
|
||||||
setSelectedView={setSelectedView}
|
setSelectedView={setSelectedView}
|
||||||
|
viewMode={ViewMode.Split}
|
||||||
|
setViewMode={setViewMode}
|
||||||
|
leftPaneView={PaneView.TopTable}
|
||||||
|
setLeftPaneView={setLeftPaneView}
|
||||||
|
rightPaneView={PaneView.FlameGraph}
|
||||||
|
setRightPaneView={setRightPaneView}
|
||||||
|
singleView={PaneView.FlameGraph}
|
||||||
|
setSingleView={setSingleView}
|
||||||
containerWidth={1600}
|
containerWidth={1600}
|
||||||
onReset={onReset}
|
onReset={onReset}
|
||||||
onTextAlignChange={jest.fn()}
|
|
||||||
textAlign={'left'}
|
|
||||||
showResetButton={true}
|
showResetButton={true}
|
||||||
colorScheme={ColorScheme.ValueBased}
|
|
||||||
onColorSchemeChange={onSchemeChange}
|
|
||||||
stickyHeader={false}
|
stickyHeader={false}
|
||||||
isDiffMode={false}
|
|
||||||
setCollapsedMap={() => {}}
|
|
||||||
collapsedMap={new CollapsedMap()}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -50,7 +53,6 @@ describe('FlameGraphHeader', () => {
|
|||||||
setSearch,
|
setSearch,
|
||||||
setSelectedView,
|
setSelectedView,
|
||||||
onReset,
|
onReset,
|
||||||
onSchemeChange,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -70,27 +72,4 @@ describe('FlameGraphHeader', () => {
|
|||||||
await userEvent.click(resetButton);
|
await userEvent.click(resetButton);
|
||||||
expect(handlers.onReset).toHaveBeenCalledTimes(1);
|
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 { ChatContextItem, OpenAssistantButton } from '@grafana/assistant';
|
||||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
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 { 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 = {
|
type Props = {
|
||||||
search: string;
|
search: string;
|
||||||
setSearch: (search: string) => void;
|
setSearch: (search: string) => void;
|
||||||
selectedView: SelectedView;
|
selectedView: SelectedView;
|
||||||
setSelectedView: (view: SelectedView) => void;
|
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;
|
containerWidth: number;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
textAlign: TextAlign;
|
|
||||||
onTextAlignChange: (align: TextAlign) => void;
|
|
||||||
showResetButton: boolean;
|
showResetButton: boolean;
|
||||||
colorScheme: ColorScheme | ColorSchemeDiff;
|
|
||||||
onColorSchemeChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
|
|
||||||
stickyHeader: boolean;
|
stickyHeader: boolean;
|
||||||
vertical?: boolean;
|
vertical?: boolean;
|
||||||
isDiffMode: boolean;
|
|
||||||
setCollapsedMap: (collapsedMap: CollapsedMap) => void;
|
|
||||||
collapsedMap: CollapsedMap;
|
|
||||||
|
|
||||||
extraHeaderElements?: React.ReactNode;
|
extraHeaderElements?: React.ReactNode;
|
||||||
|
|
||||||
@@ -40,19 +39,20 @@ const FlameGraphHeader = ({
|
|||||||
setSearch,
|
setSearch,
|
||||||
selectedView,
|
selectedView,
|
||||||
setSelectedView,
|
setSelectedView,
|
||||||
|
viewMode,
|
||||||
|
setViewMode,
|
||||||
|
leftPaneView,
|
||||||
|
setLeftPaneView,
|
||||||
|
rightPaneView,
|
||||||
|
setRightPaneView,
|
||||||
|
singleView,
|
||||||
|
setSingleView,
|
||||||
containerWidth,
|
containerWidth,
|
||||||
onReset,
|
onReset,
|
||||||
textAlign,
|
|
||||||
onTextAlignChange,
|
|
||||||
showResetButton,
|
showResetButton,
|
||||||
colorScheme,
|
|
||||||
onColorSchemeChange,
|
|
||||||
stickyHeader,
|
stickyHeader,
|
||||||
extraHeaderElements,
|
extraHeaderElements,
|
||||||
vertical,
|
vertical,
|
||||||
isDiffMode,
|
|
||||||
setCollapsedMap,
|
|
||||||
collapsedMap,
|
|
||||||
assistantContext,
|
assistantContext,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
@@ -87,6 +87,25 @@ const FlameGraphHeader = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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}>
|
<div className={styles.rightContainer}>
|
||||||
{!!assistantContext?.length && (
|
{!!assistantContext?.length && (
|
||||||
<div className={styles.buttonSpacing}>
|
<div className={styles.buttonSpacing}>
|
||||||
@@ -111,129 +130,63 @@ const FlameGraphHeader = ({
|
|||||||
aria-label={'Reset focus and sandwich state'}
|
aria-label={'Reset focus and sandwich state'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ColorSchemeButton value={colorScheme} onChange={onColorSchemeChange} isDiffMode={isDiffMode} />
|
{selectedView === SelectedView.Multi ? (
|
||||||
<ButtonGroup className={styles.buttonSpacing}>
|
<>
|
||||||
<Button
|
{viewMode === ViewMode.Single && (
|
||||||
variant={'secondary'}
|
<RadioButtonGroup<PaneView>
|
||||||
fill={'outline'}
|
size="sm"
|
||||||
size={'sm'}
|
options={paneViewOptions}
|
||||||
tooltip={'Expand all groups'}
|
value={singleView}
|
||||||
onClick={() => {
|
onChange={setSingleView}
|
||||||
setCollapsedMap(collapsedMap.setAllCollapsedStatus(false));
|
className={styles.buttonSpacing}
|
||||||
}}
|
/>
|
||||||
aria-label={'Expand all groups'}
|
)}
|
||||||
icon={'angle-double-down'}
|
<RadioButtonGroup<ViewMode>
|
||||||
disabled={selectedView === SelectedView.TopTable}
|
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>}
|
{extraHeaderElements && <div className={styles.extraElements}>{extraHeaderElements}</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type ColorSchemeButtonProps = {
|
const viewModeOptions: Array<SelectableValue<ViewMode>> = [
|
||||||
value: ColorScheme | ColorSchemeDiff;
|
{ value: ViewMode.Single, label: 'Single', description: 'Single view' },
|
||||||
onChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
|
{ value: ViewMode.Split, label: 'Split', description: 'Split view' },
|
||||||
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>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Show a bit different gradient as a way to indicate selected value
|
const paneViewOptions: Array<SelectableValue<PaneView>> = [
|
||||||
const colorDotStyle =
|
{ value: PaneView.TopTable, label: 'Top Table' },
|
||||||
{
|
{ value: PaneView.FlameGraph, label: 'Flame Graph' },
|
||||||
[ColorScheme.ValueBased]: styles.colorDotByValue,
|
{ value: PaneView.CallTree, label: 'Call Tree' },
|
||||||
[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' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function getViewOptions(width: number, vertical?: boolean): Array<SelectableValue<SelectedView>> {
|
function getViewOptions(width: number, vertical?: boolean): Array<SelectableValue<SelectedView>> {
|
||||||
let viewOptions: Array<{ value: SelectedView; label: string; description: string }> = [
|
let viewOptions: Array<{ value: SelectedView; label: string; description: string }> = [
|
||||||
{ value: SelectedView.TopTable, label: 'Top Table', description: 'Only show top table' },
|
{ value: SelectedView.TopTable, label: 'Top Table', description: 'Only show top table' },
|
||||||
{ value: SelectedView.FlameGraph, label: 'Flame Graph', description: 'Only show flame graph' },
|
{ 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) {
|
if (width >= MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH || vertical) {
|
||||||
viewOptions.push({
|
viewOptions.push({
|
||||||
value: SelectedView.Both,
|
value: SelectedView.Multi,
|
||||||
label: 'Both',
|
label: 'Multi',
|
||||||
description: 'Show both the top table and flame graph',
|
description: 'Show split or single view with multiple visualizations',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,10 +226,12 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
top: 0,
|
top: 0,
|
||||||
gap: theme.spacing(1),
|
gap: theme.spacing(1),
|
||||||
marginTop: theme.spacing(1),
|
marginTop: theme.spacing(1),
|
||||||
|
position: 'relative',
|
||||||
}),
|
}),
|
||||||
stickyHeader: css({
|
stickyHeader: css({
|
||||||
zIndex: theme.zIndex.navbarFixed,
|
zIndex: theme.zIndex.navbarFixed,
|
||||||
@@ -285,10 +240,20 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
}),
|
}),
|
||||||
inputContainer: css({
|
inputContainer: css({
|
||||||
label: 'inputContainer',
|
label: 'inputContainer',
|
||||||
flexGrow: 1,
|
flexGrow: 0,
|
||||||
minWidth: '150px',
|
minWidth: '150px',
|
||||||
maxWidth: '350px',
|
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({
|
rightContainer: css({
|
||||||
label: 'rightContainer',
|
label: 'rightContainer',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -309,44 +274,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
padding: '0 5px',
|
padding: '0 5px',
|
||||||
color: theme.colors.text.disabled,
|
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({
|
extraElements: css({
|
||||||
label: 'extraElements',
|
label: 'extraElements',
|
||||||
marginLeft: theme.spacing(1),
|
marginLeft: theme.spacing(1),
|
||||||
|
|||||||
@@ -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 FlameGraph, type Props } from './FlameGraphContainer';
|
||||||
|
export { default as FlameGraphCallTreeContainer } from './CallTree/FlameGraphCallTreeContainer';
|
||||||
export { checkFields, getMessageCheckFieldsResult } from './FlameGraph/dataTransform';
|
export { checkFields, getMessageCheckFieldsResult } from './FlameGraph/dataTransform';
|
||||||
export { data } from './FlameGraph/testData/dataNestedSet';
|
export { data } from './FlameGraph/testData/dataNestedSet';
|
||||||
|
|||||||
@@ -20,7 +20,19 @@ export enum SampleUnit {
|
|||||||
export enum SelectedView {
|
export enum SelectedView {
|
||||||
TopTable = 'topTable',
|
TopTable = 'topTable',
|
||||||
FlameGraph = 'flameGraph',
|
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 {
|
export interface TableData {
|
||||||
|
|||||||
@@ -1,14 +1,40 @@
|
|||||||
import { Decorator } from '@storybook/react';
|
import { Decorator } from '@storybook/react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { getThemeById, ThemeContext } from '@grafana/data';
|
import { createTheme, getThemeById, ThemeContext } from '@grafana/data';
|
||||||
import { GlobalStyles } from '@grafana/ui';
|
import { GlobalStyles, PortalContainer } from '@grafana/ui';
|
||||||
|
|
||||||
interface ThemeableStoryProps {
|
interface ThemeableStoryProps {
|
||||||
themeId: string;
|
themeId?: string;
|
||||||
}
|
}
|
||||||
const ThemeableStory = ({ children, themeId }: React.PropsWithChildren<ThemeableStoryProps>) => {
|
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 = `
|
const css = `
|
||||||
#storybook-root {
|
#storybook-root {
|
||||||
@@ -23,6 +49,7 @@ const ThemeableStory = ({ children, themeId }: React.PropsWithChildren<Themeable
|
|||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={theme}>
|
<ThemeContext.Provider value={theme}>
|
||||||
<GlobalStyles />
|
<GlobalStyles />
|
||||||
|
<PortalContainer />
|
||||||
|
|
||||||
<style>{css}</style>
|
<style>{css}</style>
|
||||||
{children}
|
{children}
|
||||||
@@ -33,4 +60,4 @@ const ThemeableStory = ({ children, themeId }: React.PropsWithChildren<Themeable
|
|||||||
export const withTheme =
|
export const withTheme =
|
||||||
(): Decorator =>
|
(): Decorator =>
|
||||||
// eslint-disable-next-line react/display-name
|
// 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>;
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
import * as common from '@grafana/schema';
|
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);
|
export type BucketAggregation = (DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested);
|
||||||
|
|
||||||
|
|||||||
@@ -574,8 +574,8 @@ var (
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "dashboardNewLayouts",
|
Name: "dashboardNewLayouts",
|
||||||
Description: "Enables experimental new dashboard layouts",
|
Description: "Enables new dashboard layouts",
|
||||||
Stage: FeatureStageExperimental,
|
Stage: FeatureStagePublicPreview,
|
||||||
FrontendOnly: false, // The restore backend feature changes behavior based on this flag
|
FrontendOnly: false, // The restore backend feature changes behavior based on this flag
|
||||||
Owner: grafanaDashboardsSquad,
|
Owner: grafanaDashboardsSquad,
|
||||||
},
|
},
|
||||||
@@ -879,6 +879,13 @@ var (
|
|||||||
Owner: grafanaAlertingSquad,
|
Owner: grafanaAlertingSquad,
|
||||||
FrontendOnly: true,
|
FrontendOnly: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "alertingNavigationV2",
|
||||||
|
Description: "Enables the new Alerting navigation structure with improved menu grouping",
|
||||||
|
Stage: FeatureStageExperimental,
|
||||||
|
Owner: grafanaAlertingSquad,
|
||||||
|
FrontendOnly: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "alertingSavedSearches",
|
Name: "alertingSavedSearches",
|
||||||
Description: "Enables saved searches for alert rules list",
|
Description: "Enables saved searches for alert rules list",
|
||||||
@@ -981,7 +988,8 @@ var (
|
|||||||
Stage: FeatureStageDeprecated,
|
Stage: FeatureStageDeprecated,
|
||||||
Owner: grafanaPartnerPluginsSquad,
|
Owner: grafanaPartnerPluginsSquad,
|
||||||
Expression: "true", // Enabled by default for now
|
Expression: "true", // Enabled by default for now
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
Name: "alertingFilterV2",
|
Name: "alertingFilterV2",
|
||||||
Description: "Enable the new alerting search experience",
|
Description: "Enable the new alerting search experience",
|
||||||
Stage: FeatureStageExperimental,
|
Stage: FeatureStageExperimental,
|
||||||
@@ -2069,6 +2077,14 @@ var (
|
|||||||
Owner: grafanaObservabilityTracesAndProfilingSquad,
|
Owner: grafanaObservabilityTracesAndProfilingSquad,
|
||||||
FrontendOnly: false,
|
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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Generated
+3
-1
@@ -79,7 +79,7 @@ annotationPermissionUpdate,GA,@grafana/identity-access-team,false,false,false
|
|||||||
dashboardSceneForViewers,GA,@grafana/dashboards-squad,false,false,true
|
dashboardSceneForViewers,GA,@grafana/dashboards-squad,false,false,true
|
||||||
dashboardSceneSolo,GA,@grafana/dashboards-squad,false,false,true
|
dashboardSceneSolo,GA,@grafana/dashboards-squad,false,false,true
|
||||||
dashboardScene,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
|
dashboardUndoRedo,experimental,@grafana/dashboards-squad,false,false,true
|
||||||
unlimitedLayoutsNesting,experimental,@grafana/dashboards-squad,false,false,true
|
unlimitedLayoutsNesting,experimental,@grafana/dashboards-squad,false,false,true
|
||||||
drilldownRecommendations,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
|
suggestedDashboards,experimental,@grafana/sharing-squad,false,false,false
|
||||||
dashboardTemplates,preview,@grafana/sharing-squad,false,false,false
|
dashboardTemplates,preview,@grafana/sharing-squad,false,false,false
|
||||||
alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true
|
alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true
|
||||||
|
alertingNavigationV2,experimental,@grafana/alerting-squad,false,false,false
|
||||||
alertingSavedSearches,experimental,@grafana/alerting-squad,false,false,true
|
alertingSavedSearches,experimental,@grafana/alerting-squad,false,false,true
|
||||||
alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false
|
alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false
|
||||||
preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-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
|
smoothingTransformation,experimental,@grafana/datapro,false,false,true
|
||||||
secretsManagementAppPlatformAwsKeeper,experimental,@grafana/grafana-operator-experience-squad,false,false,false
|
secretsManagementAppPlatformAwsKeeper,experimental,@grafana/grafana-operator-experience-squad,false,false,false
|
||||||
profilesExemplars,experimental,@grafana/observability-traces-and-profiling,false,false,false
|
profilesExemplars,experimental,@grafana/observability-traces-and-profiling,false,false,false
|
||||||
|
alertingSyncDispatchTimer,experimental,@grafana/alerting-squad,false,true,false
|
||||||
|
|||||||
|
Generated
+9
-1
@@ -260,7 +260,7 @@ const (
|
|||||||
FlagAnnotationPermissionUpdate = "annotationPermissionUpdate"
|
FlagAnnotationPermissionUpdate = "annotationPermissionUpdate"
|
||||||
|
|
||||||
// FlagDashboardNewLayouts
|
// FlagDashboardNewLayouts
|
||||||
// Enables experimental new dashboard layouts
|
// Enables new dashboard layouts
|
||||||
FlagDashboardNewLayouts = "dashboardNewLayouts"
|
FlagDashboardNewLayouts = "dashboardNewLayouts"
|
||||||
|
|
||||||
// FlagPdfTables
|
// FlagPdfTables
|
||||||
@@ -371,6 +371,10 @@ const (
|
|||||||
// Enables a flow to get started with a new dashboard from a template
|
// Enables a flow to get started with a new dashboard from a template
|
||||||
FlagDashboardTemplates = "dashboardTemplates"
|
FlagDashboardTemplates = "dashboardTemplates"
|
||||||
|
|
||||||
|
// FlagAlertingNavigationV2
|
||||||
|
// Enables the new Alerting navigation structure with improved menu grouping
|
||||||
|
FlagAlertingNavigationV2 = "alertingNavigationV2"
|
||||||
|
|
||||||
// FlagAlertingDisableSendAlertsExternal
|
// FlagAlertingDisableSendAlertsExternal
|
||||||
// Disables the ability to send alerts to an external Alertmanager datasource.
|
// Disables the ability to send alerts to an external Alertmanager datasource.
|
||||||
FlagAlertingDisableSendAlertsExternal = "alertingDisableSendAlertsExternal"
|
FlagAlertingDisableSendAlertsExternal = "alertingDisableSendAlertsExternal"
|
||||||
@@ -789,4 +793,8 @@ const (
|
|||||||
// FlagProfilesExemplars
|
// FlagProfilesExemplars
|
||||||
// Enables profiles exemplars support in profiles drilldown
|
// Enables profiles exemplars support in profiles drilldown
|
||||||
FlagProfilesExemplars = "profilesExemplars"
|
FlagProfilesExemplars = "profilesExemplars"
|
||||||
|
|
||||||
|
// FlagAlertingSyncDispatchTimer
|
||||||
|
// Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods
|
||||||
|
FlagAlertingSyncDispatchTimer = "alertingSyncDispatchTimer"
|
||||||
)
|
)
|
||||||
|
|||||||
+35
-5
@@ -348,6 +348,18 @@
|
|||||||
"expression": "true"
|
"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": {
|
"metadata": {
|
||||||
"name": "alertingNotificationHistory",
|
"name": "alertingNotificationHistory",
|
||||||
@@ -511,6 +523,20 @@
|
|||||||
"frontend": true
|
"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": {
|
"metadata": {
|
||||||
"name": "alertingTriage",
|
"name": "alertingTriage",
|
||||||
@@ -662,7 +688,8 @@
|
|||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "auditLoggingAppPlatform",
|
"name": "auditLoggingAppPlatform",
|
||||||
"resourceVersion": "1767013056996",
|
"resourceVersion": "1767013056996",
|
||||||
"creationTimestamp": "2025-12-29T12:57:36Z"
|
"creationTimestamp": "2025-12-29T12:57:36Z",
|
||||||
|
"deletionTimestamp": "2026-01-06T09:18:36Z"
|
||||||
},
|
},
|
||||||
"spec": {
|
"spec": {
|
||||||
"description": "Enable audit logging with Kubernetes under app platform",
|
"description": "Enable audit logging with Kubernetes under app platform",
|
||||||
@@ -1015,12 +1042,15 @@
|
|||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "dashboardNewLayouts",
|
"name": "dashboardNewLayouts",
|
||||||
"resourceVersion": "1764664939750",
|
"resourceVersion": "1768382835527",
|
||||||
"creationTimestamp": "2024-10-23T08:55:45Z"
|
"creationTimestamp": "2024-10-23T08:55:45Z",
|
||||||
|
"annotations": {
|
||||||
|
"grafana.app/updatedTimestamp": "2026-01-14 09:27:15.527103 +0000 UTC"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"spec": {
|
"spec": {
|
||||||
"description": "Enables experimental new dashboard layouts",
|
"description": "Enables new dashboard layouts",
|
||||||
"stage": "experimental",
|
"stage": "preview",
|
||||||
"codeowner": "@grafana/dashboards-squad"
|
"codeowner": "@grafana/dashboards-squad"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
|
|||||||
}
|
}
|
||||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||||
if c.HasRole(identity.RoleAdmin) &&
|
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{
|
generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
|
||||||
Text: "Provisioning",
|
Text: "Provisioning",
|
||||||
Id: "provisioning",
|
Id: "provisioning",
|
||||||
|
|||||||
@@ -213,6 +213,9 @@ func (ng *AlertNG) init() error {
|
|||||||
SkipVerify: ng.Cfg.Smtp.SkipVerify,
|
SkipVerify: ng.Cfg.Smtp.SkipVerify,
|
||||||
StaticHeaders: ng.Cfg.Smtp.StaticHeaders,
|
StaticHeaders: ng.Cfg.Smtp.StaticHeaders,
|
||||||
}
|
}
|
||||||
|
runtimeConfig := remoteClient.RuntimeConfig{
|
||||||
|
DispatchTimer: notifier.GetDispatchTimer(ng.FeatureToggles).String(),
|
||||||
|
}
|
||||||
|
|
||||||
cfg := remote.AlertmanagerConfig{
|
cfg := remote.AlertmanagerConfig{
|
||||||
BasicAuthPassword: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.Password,
|
BasicAuthPassword: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.Password,
|
||||||
@@ -222,6 +225,7 @@ func (ng *AlertNG) init() error {
|
|||||||
ExternalURL: ng.Cfg.AppURL,
|
ExternalURL: ng.Cfg.AppURL,
|
||||||
SmtpConfig: smtpCfg,
|
SmtpConfig: smtpCfg,
|
||||||
Timeout: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.Timeout,
|
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 {
|
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)
|
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.
|
// How long we keep silences in the kvstore after they've expired.
|
||||||
silenceRetention = 5 * 24 * time.Hour
|
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 {
|
type AlertingStore interface {
|
||||||
@@ -44,8 +47,10 @@ type AlertingStore interface {
|
|||||||
type stateStore interface {
|
type stateStore interface {
|
||||||
SaveSilences(ctx context.Context, st alertingNotify.State) (int64, error)
|
SaveSilences(ctx context.Context, st alertingNotify.State) (int64, error)
|
||||||
SaveNotificationLog(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)
|
GetSilences(ctx context.Context) (string, error)
|
||||||
GetNotificationLog(ctx context.Context) (string, error)
|
GetNotificationLog(ctx context.Context) (string, error)
|
||||||
|
GetFlushLog(ctx context.Context) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type alertmanager struct {
|
type alertmanager struct {
|
||||||
@@ -101,6 +106,10 @@ func NewAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
flushLog, err := stateStore.GetFlushLog(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
silencesOptions := maintenanceOptions{
|
silencesOptions := maintenanceOptions{
|
||||||
initialState: silences,
|
initialState: silences,
|
||||||
@@ -123,12 +132,29 @@ func NewAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A
|
|||||||
}
|
}
|
||||||
l := log.New("ngalert.notifier")
|
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{
|
opts := alertingNotify.GrafanaAlertmanagerOpts{
|
||||||
ExternalURL: cfg.AppURL,
|
ExternalURL: cfg.AppURL,
|
||||||
AlertStoreCallback: nil,
|
AlertStoreCallback: nil,
|
||||||
PeerTimeout: cfg.UnifiedAlerting.HAPeerTimeout,
|
PeerTimeout: cfg.UnifiedAlerting.HAPeerTimeout,
|
||||||
Silences: silencesOptions,
|
Silences: silencesOptions,
|
||||||
Nflog: nflogOptions,
|
Nflog: nflogOptions,
|
||||||
|
FlushLog: flushLogOptions,
|
||||||
|
DispatchTimer: dispatchTimer,
|
||||||
Limits: alertingNotify.Limits{
|
Limits: alertingNotify.Limits{
|
||||||
MaxSilences: cfg.UnifiedAlerting.AlertmanagerMaxSilencesCount,
|
MaxSilences: cfg.UnifiedAlerting.AlertmanagerMaxSilencesCount,
|
||||||
MaxSilenceSizeBytes: cfg.UnifiedAlerting.AlertmanagerMaxSilenceSizeBytes,
|
MaxSilenceSizeBytes: cfg.UnifiedAlerting.AlertmanagerMaxSilenceSizeBytes,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
KVNamespace = "alertmanager"
|
||||||
NotificationLogFilename = "notifications"
|
NotificationLogFilename = "notifications"
|
||||||
SilencesFilename = "silences"
|
SilencesFilename = "silences"
|
||||||
|
FlushLogFilename = "flushes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileStore is in charge of persisting the alertmanager files to the database.
|
// 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)
|
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.
|
// contentFor returns the content for the given Alertmanager kvstore key.
|
||||||
func (fileStore *FileStore) contentFor(ctx context.Context, filename string) (string, error) {
|
func (fileStore *FileStore) contentFor(ctx context.Context, filename string) (string, error) {
|
||||||
// Then, let's attempt to read it from the database.
|
// 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)
|
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.
|
// 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) {
|
func (fileStore *FileStore) persist(ctx context.Context, filename string, st alertingNotify.State) (int64, error) {
|
||||||
var size int64
|
var size int64
|
||||||
|
|||||||
@@ -106,3 +106,48 @@ func TestFileStore_NotificationLog(t *testing.T) {
|
|||||||
t.Errorf("Unexpected Diff: %v", cmp.Diff(newState, decoded))
|
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 {
|
type ExternalState struct {
|
||||||
Silences []byte
|
Silences []byte
|
||||||
Nflog []byte
|
Nflog []byte
|
||||||
|
FlushLog []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// StateMerger describes a type that is able to merge external state (nflog, silences) with its own.
|
// 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,
|
func (moa *MultiOrgAlertmanager) cleanupOrphanLocalOrgState(ctx context.Context,
|
||||||
activeOrganizations map[int64]struct{},
|
activeOrganizations map[int64]struct{},
|
||||||
) {
|
) {
|
||||||
storedFiles := []string{NotificationLogFilename, SilencesFilename}
|
storedFiles := []string{NotificationLogFilename, SilencesFilename, FlushLogFilename}
|
||||||
for _, fileName := range storedFiles {
|
for _, fileName := range storedFiles {
|
||||||
keys, err := moa.kvStore.Keys(ctx, kvstore.AllOrganizations, KVNamespace, fileName)
|
keys, err := moa.kvStore.Keys(ctx, kvstore.AllOrganizations, KVNamespace, fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -5,5 +5,8 @@ func (am *alertmanager) MergeState(state ExternalState) error {
|
|||||||
if err := am.Base.MergeNflog(state.Nflog); err != nil {
|
if err := am.Base.MergeNflog(state.Nflog); err != nil {
|
||||||
return err
|
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"
|
"time"
|
||||||
|
|
||||||
"github.com/matttproud/golang_protobuf_extensions/pbutil"
|
"github.com/matttproud/golang_protobuf_extensions/pbutil"
|
||||||
|
"github.com/prometheus/alertmanager/flushlog/flushlogpb"
|
||||||
"github.com/prometheus/alertmanager/nflog/nflogpb"
|
"github.com/prometheus/alertmanager/nflog/nflogpb"
|
||||||
"github.com/prometheus/alertmanager/silence/silencepb"
|
"github.com/prometheus/alertmanager/silence/silencepb"
|
||||||
"github.com/prometheus/common/model"
|
"github.com/prometheus/common/model"
|
||||||
@@ -228,15 +229,13 @@ func (f *FakeOrgStore) FetchOrgIds(_ context.Context) ([]int64, error) {
|
|||||||
return f.orgs, nil
|
return f.orgs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type NoValidation struct {
|
type NoValidation struct{}
|
||||||
}
|
|
||||||
|
|
||||||
func (n NoValidation) Validate(_ models.NotificationSettings) error {
|
func (n NoValidation) Validate(_ models.NotificationSettings) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type RejectingValidation struct {
|
type RejectingValidation struct{}
|
||||||
}
|
|
||||||
|
|
||||||
func (n RejectingValidation) Validate(s models.NotificationSettings) error {
|
func (n RejectingValidation) Validate(s models.NotificationSettings) error {
|
||||||
return ErrorReceiverDoesNotExist{ErrorReferenceInvalid: ErrorReferenceInvalid{Reference: s.Receiver}}
|
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 {
|
type call struct {
|
||||||
Method string
|
Method string
|
||||||
Args []interface{}
|
Args []interface{}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import (
|
|||||||
type stateStore interface {
|
type stateStore interface {
|
||||||
GetSilences(ctx context.Context) (string, error)
|
GetSilences(ctx context.Context) (string, error)
|
||||||
GetNotificationLog(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.
|
// AutogenFn is a function that adds auto-generated routes to a configuration.
|
||||||
@@ -86,6 +87,8 @@ type Alertmanager struct {
|
|||||||
|
|
||||||
promoteConfig bool
|
promoteConfig bool
|
||||||
externalURL string
|
externalURL string
|
||||||
|
|
||||||
|
runtimeConfig remoteClient.RuntimeConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertmanagerConfig struct {
|
type AlertmanagerConfig struct {
|
||||||
@@ -111,6 +114,9 @@ type AlertmanagerConfig struct {
|
|||||||
|
|
||||||
// Timeout for the HTTP client.
|
// Timeout for the HTTP client.
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
|
|
||||||
|
// RuntimeConfig specifies runtime behavior settings for the remote Alertmanager.
|
||||||
|
RuntimeConfig remoteClient.RuntimeConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AlertmanagerConfig) Validate() error {
|
func (cfg *AlertmanagerConfig) Validate() error {
|
||||||
@@ -203,6 +209,7 @@ func NewAlertmanager(ctx context.Context, cfg AlertmanagerConfig, store stateSto
|
|||||||
externalURL: cfg.ExternalURL,
|
externalURL: cfg.ExternalURL,
|
||||||
promoteConfig: cfg.PromoteConfig,
|
promoteConfig: cfg.PromoteConfig,
|
||||||
smtp: cfg.SmtpConfig,
|
smtp: cfg.SmtpConfig,
|
||||||
|
runtimeConfig: cfg.RuntimeConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the default configuration once and remember its hash so we can compare it later.
|
// 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,
|
AlertmanagerConfig: mergeResult.Config,
|
||||||
Templates: templates,
|
Templates: templates,
|
||||||
},
|
},
|
||||||
CreatedAt: createdAtEpoch,
|
CreatedAt: createdAtEpoch,
|
||||||
Promoted: am.promoteConfig,
|
Promoted: am.promoteConfig,
|
||||||
ExternalURL: am.externalURL,
|
ExternalURL: am.externalURL,
|
||||||
SmtpConfig: am.smtp,
|
SmtpConfig: am.smtp,
|
||||||
|
RuntimeConfig: am.runtimeConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
cfgHash, err := calculateUserGrafanaConfigHash(payload)
|
cfgHash, err := calculateUserGrafanaConfigHash(payload)
|
||||||
@@ -388,6 +396,8 @@ func (am *Alertmanager) GetRemoteState(ctx context.Context) (notifier.ExternalSt
|
|||||||
rs.Silences = p.Data
|
rs.Silences = p.Data
|
||||||
case "nfl":
|
case "nfl":
|
||||||
rs.Nflog = p.Data
|
rs.Nflog = p.Data
|
||||||
|
case "fls":
|
||||||
|
rs.FlushLog = p.Data
|
||||||
default:
|
default:
|
||||||
return rs, fmt.Errorf("unknown part key %q", p.Key)
|
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)})
|
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{
|
fs := alertingClusterPB.FullState{
|
||||||
Parts: parts,
|
Parts: parts,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ func (u *GrafanaAlertmanagerConfig) MarshalJSON() ([]byte, error) {
|
|||||||
return definition.MarshalJSONWithSecrets((*cfg)(u))
|
return definition.MarshalJSONWithSecrets((*cfg)(u))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RuntimeConfig struct {
|
||||||
|
DispatchTimer string `json:"dispatch_timer"`
|
||||||
|
}
|
||||||
|
|
||||||
type UserGrafanaConfig struct {
|
type UserGrafanaConfig struct {
|
||||||
GrafanaAlertmanagerConfig GrafanaAlertmanagerConfig `json:"configuration"`
|
GrafanaAlertmanagerConfig GrafanaAlertmanagerConfig `json:"configuration"`
|
||||||
Hash string `json:"configuration_hash"`
|
Hash string `json:"configuration_hash"`
|
||||||
@@ -37,6 +41,7 @@ type UserGrafanaConfig struct {
|
|||||||
Promoted bool `json:"promoted"`
|
Promoted bool `json:"promoted"`
|
||||||
ExternalURL string `json:"external_url"`
|
ExternalURL string `json:"external_url"`
|
||||||
SmtpConfig SmtpConfig `json:"smtp_config"`
|
SmtpConfig SmtpConfig `json:"smtp_config"`
|
||||||
|
RuntimeConfig RuntimeConfig `json:"runtime_config"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mc *Mimir) GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafanaConfig, error) {
|
func (mc *Mimir) GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafanaConfig, error) {
|
||||||
|
|||||||
@@ -600,6 +600,7 @@ type Cfg struct {
|
|||||||
IndexRebuildInterval time.Duration
|
IndexRebuildInterval time.Duration
|
||||||
IndexCacheTTL time.Duration
|
IndexCacheTTL time.Duration
|
||||||
IndexMinUpdateInterval time.Duration // Don't update index if it was updated less than this interval ago.
|
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.
|
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.
|
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
|
EnableSharding bool
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ func (cfg *Cfg) setUnifiedStorageConfig() {
|
|||||||
cfg.IndexRebuildInterval = section.Key("index_rebuild_interval").MustDuration(24 * time.Hour)
|
cfg.IndexRebuildInterval = section.Key("index_rebuild_interval").MustDuration(24 * time.Hour)
|
||||||
cfg.IndexCacheTTL = section.Key("index_cache_ttl").MustDuration(10 * time.Minute)
|
cfg.IndexCacheTTL = section.Key("index_cache_ttl").MustDuration(10 * time.Minute)
|
||||||
cfg.IndexMinUpdateInterval = section.Key("index_min_update_interval").MustDuration(0)
|
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.SprinklesApiServer = section.Key("sprinkles_api_server").String()
|
||||||
cfg.SprinklesApiServerPageLimit = section.Key("sprinkles_api_server_page_limit").MustInt(10000)
|
cfg.SprinklesApiServerPageLimit = section.Key("sprinkles_api_server_page_limit").MustInt(10000)
|
||||||
cfg.CACertPath = section.Key("ca_cert_path").String()
|
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
|
// 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
|
// It should be implemented with efficient indexes and does not need read-after-write semantics
|
||||||
service ResourceIndex {
|
service ResourceIndex {
|
||||||
|
// Query for documents
|
||||||
rpc Search(ResourceSearchRequest) returns (ResourceSearchResponse);
|
rpc Search(ResourceSearchRequest) returns (ResourceSearchResponse);
|
||||||
|
|
||||||
// Get the resource stats
|
// Get the resource stats
|
||||||
rpc GetStats(ResourceStatsRequest) returns (ResourceStatsResponse);
|
rpc GetStats(ResourceStatsRequest) returns (ResourceStatsResponse);
|
||||||
|
|
||||||
|
// Rebuild the search index
|
||||||
rpc RebuildIndexes(RebuildIndexesRequest) returns (RebuildIndexesResponse);
|
rpc RebuildIndexes(RebuildIndexesRequest) returns (RebuildIndexesResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +51,20 @@ message ResourceStatsResponse {
|
|||||||
repeated Stats stats = 2;
|
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
|
// Search within a single resource
|
||||||
message ResourceSearchRequest {
|
message ResourceSearchRequest {
|
||||||
message Sort {
|
message Sort {
|
||||||
@@ -64,6 +80,18 @@ message ResourceSearchRequest {
|
|||||||
// date queries
|
// 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
|
// The key must include namespace + group + resource
|
||||||
ListOptions options = 1;
|
ListOptions options = 1;
|
||||||
|
|
||||||
@@ -99,6 +127,9 @@ message ResourceSearchRequest {
|
|||||||
int64 page = 11;
|
int64 page = 11;
|
||||||
|
|
||||||
int64 permission = 12;
|
int64 permission = 12;
|
||||||
|
|
||||||
|
// Optionally specify which fields are included in the query
|
||||||
|
repeated QueryField query_fields = 13;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ResourceSearchResponse {
|
message ResourceSearchResponse {
|
||||||
|
|||||||
@@ -290,7 +290,6 @@ const SEARCH_FIELD_NAMESPACE = "namespace"
|
|||||||
const SEARCH_FIELD_NAME = "name"
|
const SEARCH_FIELD_NAME = "name"
|
||||||
const SEARCH_FIELD_RV = "rv"
|
const SEARCH_FIELD_RV = "rv"
|
||||||
const SEARCH_FIELD_TITLE = "title"
|
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_TITLE_PHRASE = "title_phrase" // filtering/sorting on title by full phrase
|
||||||
const SEARCH_FIELD_DESCRIPTION = "description"
|
const SEARCH_FIELD_DESCRIPTION = "description"
|
||||||
const SEARCH_FIELD_TAGS = "tags"
|
const SEARCH_FIELD_TAGS = "tags"
|
||||||
|
|||||||
@@ -21,6 +21,65 @@ const (
|
|||||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
_ = 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
|
// Get statistics across multiple resources
|
||||||
// For these queries, we do not need authorization to see the actual values
|
// For these queries, we do not need authorization to see the actual values
|
||||||
type ResourceStatsRequest struct {
|
type ResourceStatsRequest struct {
|
||||||
@@ -165,10 +224,12 @@ type ResourceSearchRequest struct {
|
|||||||
// the return fields (empty will return everything)
|
// the return fields (empty will return everything)
|
||||||
Fields []string `protobuf:"bytes,8,rep,name=fields,proto3" json:"fields,omitempty"`
|
Fields []string `protobuf:"bytes,8,rep,name=fields,proto3" json:"fields,omitempty"`
|
||||||
// explain each result (added to the each row)
|
// explain each result (added to the each row)
|
||||||
Explain bool `protobuf:"varint,9,opt,name=explain,proto3" json:"explain,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"`
|
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"`
|
Page int64 `protobuf:"varint,11,opt,name=page,proto3" json:"page,omitempty"`
|
||||||
Permission int64 `protobuf:"varint,12,opt,name=permission,proto3" json:"permission,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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@@ -287,6 +348,13 @@ func (x *ResourceSearchRequest) GetPermission() int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *ResourceSearchRequest) GetQueryFields() []*ResourceSearchRequest_QueryField {
|
||||||
|
if x != nil {
|
||||||
|
return x.QueryFields
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type ResourceSearchResponse struct {
|
type ResourceSearchResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
// Error details
|
// Error details
|
||||||
@@ -670,6 +738,70 @@ func (x *ResourceSearchRequest_Facet) GetLimit() int64 {
|
|||||||
return 0
|
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 {
|
type ResourceSearchResponse_Facet struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Field string `protobuf:"bytes,1,opt,name=field,proto3" json:"field,omitempty"`
|
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() {
|
func (x *ResourceSearchResponse_Facet) Reset() {
|
||||||
*x = ResourceSearchResponse_Facet{}
|
*x = ResourceSearchResponse_Facet{}
|
||||||
mi := &file_search_proto_msgTypes[10]
|
mi := &file_search_proto_msgTypes[11]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -697,7 +829,7 @@ func (x *ResourceSearchResponse_Facet) String() string {
|
|||||||
func (*ResourceSearchResponse_Facet) ProtoMessage() {}
|
func (*ResourceSearchResponse_Facet) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ResourceSearchResponse_Facet) ProtoReflect() protoreflect.Message {
|
func (x *ResourceSearchResponse_Facet) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_search_proto_msgTypes[10]
|
mi := &file_search_proto_msgTypes[11]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -751,7 +883,7 @@ type ResourceSearchResponse_TermFacet struct {
|
|||||||
|
|
||||||
func (x *ResourceSearchResponse_TermFacet) Reset() {
|
func (x *ResourceSearchResponse_TermFacet) Reset() {
|
||||||
*x = ResourceSearchResponse_TermFacet{}
|
*x = ResourceSearchResponse_TermFacet{}
|
||||||
mi := &file_search_proto_msgTypes[11]
|
mi := &file_search_proto_msgTypes[12]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -763,7 +895,7 @@ func (x *ResourceSearchResponse_TermFacet) String() string {
|
|||||||
func (*ResourceSearchResponse_TermFacet) ProtoMessage() {}
|
func (*ResourceSearchResponse_TermFacet) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ResourceSearchResponse_TermFacet) ProtoReflect() protoreflect.Message {
|
func (x *ResourceSearchResponse_TermFacet) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_search_proto_msgTypes[11]
|
mi := &file_search_proto_msgTypes[12]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x4d, 0x0a, 0x0c, 0x71, 0x75,
|
||||||
0x72, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
|
0x65, 0x72, 0x79, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b,
|
||||||
0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63,
|
0x32, 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f,
|
||||||
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,
|
|
||||||
0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
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,
|
0x74, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x0b, 0x71, 0x75,
|
||||||
0x38, 0x01, 0x22, 0xea, 0x04, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53,
|
0x65, 0x72, 0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, 0x72,
|
||||||
0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a,
|
0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72,
|
0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18,
|
||||||
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73,
|
0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x1a, 0x33, 0x0a, 0x05, 0x46,
|
||||||
0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65,
|
0x61, 0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20,
|
||||||
0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
|
0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69,
|
||||||
0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03,
|
0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74,
|
||||||
0x6b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x03,
|
0x1a, 0x64, 0x0a, 0x0a, 0x51, 0x75, 0x65, 0x72, 0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12,
|
||||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
|
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
|
||||||
0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x07, 0x72,
|
0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e,
|
||||||
0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f,
|
0x32, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x51, 0x75, 0x65, 0x72,
|
||||||
0x68, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61,
|
0x79, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65,
|
||||||
0x6c, 0x48, 0x69, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x63,
|
0x12, 0x14, 0x0a, 0x05, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x52,
|
||||||
0x6f, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x71, 0x75, 0x65, 0x72, 0x79,
|
0x05, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x1a, 0x5f, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45,
|
||||||
0x43, 0x6f, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x63, 0x6f, 0x72,
|
0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x53, 0x63, 0x6f, 0x72,
|
0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18,
|
||||||
0x65, 0x12, 0x41, 0x0a, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b,
|
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
||||||
0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f,
|
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,
|
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,
|
0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x61, 0x63, 0x65, 0x74, 0x1a, 0x8f, 0x01, 0x0a, 0x05, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x14,
|
0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72,
|
||||||
0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66,
|
0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12,
|
||||||
0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20,
|
0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72,
|
||||||
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,
|
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,
|
0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75,
|
||||||
0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22,
|
0x6c, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f,
|
||||||
0x60, 0x0a, 0x15, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65,
|
0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x61, 0x62,
|
||||||
0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65,
|
0x6c, 0x65, 0x52, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74,
|
||||||
0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d,
|
0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52,
|
||||||
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x29, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x02,
|
0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x48, 0x69, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x71, 0x75,
|
||||||
0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
|
0x65, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09,
|
||||||
0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x04, 0x6b, 0x65, 0x79,
|
0x71, 0x75, 0x65, 0x72, 0x79, 0x43, 0x6f, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78,
|
||||||
0x73, 0x22, 0x83, 0x01, 0x0a, 0x16, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64,
|
0x5f, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x6d, 0x61,
|
||||||
0x65, 0x78, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c,
|
0x78, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x41, 0x0a, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x18,
|
||||||
0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01,
|
0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
||||||
0x28, 0x03, 0x52, 0x0c, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74,
|
0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52,
|
||||||
0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28,
|
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74,
|
||||||
0x09, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72,
|
0x72, 0x79, 0x52, 0x05, 0x66, 0x61, 0x63, 0x65, 0x74, 0x1a, 0x8f, 0x01, 0x0a, 0x05, 0x46, 0x61,
|
||||||
0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f,
|
0x63, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||||
0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74,
|
0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74,
|
||||||
0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x32, 0xfe, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f,
|
0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12,
|
||||||
0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x4b, 0x0a, 0x06, 0x53, 0x65, 0x61,
|
0x18, 0x0a, 0x07, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
|
||||||
0x72, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52,
|
0x52, 0x07, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x12, 0x40, 0x0a, 0x05, 0x74, 0x65, 0x72,
|
||||||
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71,
|
0x6d, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
|
||||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
|
0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72,
|
||||||
0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65,
|
0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x46,
|
||||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61,
|
0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x1a, 0x35, 0x0a, 0x09, 0x54,
|
||||||
0x74, 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65,
|
0x65, 0x72, 0x6d, 0x46, 0x61, 0x63, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d,
|
||||||
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05,
|
||||||
0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65,
|
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75,
|
||||||
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
0x6e, 0x74, 0x1a, 0x60, 0x0a, 0x0a, 0x46, 0x61, 0x63, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79,
|
||||||
0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x0e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e,
|
0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
|
||||||
0x64, 0x65, 0x78, 0x65, 0x73, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
0x65, 0x79, 0x12, 0x3c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||||
0x2e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52,
|
0x0b, 0x32, 0x26, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73,
|
||||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
|
0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||||
0x65, 0x2e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73,
|
0x6e, 0x73, 0x65, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
|
||||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68,
|
0x3a, 0x02, 0x38, 0x01, 0x22, 0x60, 0x0a, 0x15, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49,
|
||||||
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67,
|
0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a,
|
||||||
0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61,
|
0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75,
|
0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x29, 0x0a, 0x04, 0x6b,
|
||||||
0x72, 0x63, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
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 (
|
var (
|
||||||
@@ -947,53 +1095,58 @@ func file_search_proto_rawDescGZIP() []byte {
|
|||||||
return file_search_proto_rawDescData
|
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{
|
var file_search_proto_goTypes = []any{
|
||||||
(*ResourceStatsRequest)(nil), // 0: resource.ResourceStatsRequest
|
(QueryFieldType)(0), // 0: resource.QueryFieldType
|
||||||
(*ResourceStatsResponse)(nil), // 1: resource.ResourceStatsResponse
|
(*ResourceStatsRequest)(nil), // 1: resource.ResourceStatsRequest
|
||||||
(*ResourceSearchRequest)(nil), // 2: resource.ResourceSearchRequest
|
(*ResourceStatsResponse)(nil), // 2: resource.ResourceStatsResponse
|
||||||
(*ResourceSearchResponse)(nil), // 3: resource.ResourceSearchResponse
|
(*ResourceSearchRequest)(nil), // 3: resource.ResourceSearchRequest
|
||||||
(*RebuildIndexesRequest)(nil), // 4: resource.RebuildIndexesRequest
|
(*ResourceSearchResponse)(nil), // 4: resource.ResourceSearchResponse
|
||||||
(*RebuildIndexesResponse)(nil), // 5: resource.RebuildIndexesResponse
|
(*RebuildIndexesRequest)(nil), // 5: resource.RebuildIndexesRequest
|
||||||
(*ResourceStatsResponse_Stats)(nil), // 6: resource.ResourceStatsResponse.Stats
|
(*RebuildIndexesResponse)(nil), // 6: resource.RebuildIndexesResponse
|
||||||
(*ResourceSearchRequest_Sort)(nil), // 7: resource.ResourceSearchRequest.Sort
|
(*ResourceStatsResponse_Stats)(nil), // 7: resource.ResourceStatsResponse.Stats
|
||||||
(*ResourceSearchRequest_Facet)(nil), // 8: resource.ResourceSearchRequest.Facet
|
(*ResourceSearchRequest_Sort)(nil), // 8: resource.ResourceSearchRequest.Sort
|
||||||
nil, // 9: resource.ResourceSearchRequest.FacetEntry
|
(*ResourceSearchRequest_Facet)(nil), // 9: resource.ResourceSearchRequest.Facet
|
||||||
(*ResourceSearchResponse_Facet)(nil), // 10: resource.ResourceSearchResponse.Facet
|
(*ResourceSearchRequest_QueryField)(nil), // 10: resource.ResourceSearchRequest.QueryField
|
||||||
(*ResourceSearchResponse_TermFacet)(nil), // 11: resource.ResourceSearchResponse.TermFacet
|
nil, // 11: resource.ResourceSearchRequest.FacetEntry
|
||||||
nil, // 12: resource.ResourceSearchResponse.FacetEntry
|
(*ResourceSearchResponse_Facet)(nil), // 12: resource.ResourceSearchResponse.Facet
|
||||||
(*ErrorResult)(nil), // 13: resource.ErrorResult
|
(*ResourceSearchResponse_TermFacet)(nil), // 13: resource.ResourceSearchResponse.TermFacet
|
||||||
(*ListOptions)(nil), // 14: resource.ListOptions
|
nil, // 14: resource.ResourceSearchResponse.FacetEntry
|
||||||
(*ResourceKey)(nil), // 15: resource.ResourceKey
|
(*ErrorResult)(nil), // 15: resource.ErrorResult
|
||||||
(*ResourceTable)(nil), // 16: resource.ResourceTable
|
(*ListOptions)(nil), // 16: resource.ListOptions
|
||||||
|
(*ResourceKey)(nil), // 17: resource.ResourceKey
|
||||||
|
(*ResourceTable)(nil), // 18: resource.ResourceTable
|
||||||
}
|
}
|
||||||
var file_search_proto_depIdxs = []int32{
|
var file_search_proto_depIdxs = []int32{
|
||||||
13, // 0: resource.ResourceStatsResponse.error:type_name -> resource.ErrorResult
|
15, // 0: resource.ResourceStatsResponse.error:type_name -> resource.ErrorResult
|
||||||
6, // 1: resource.ResourceStatsResponse.stats:type_name -> resource.ResourceStatsResponse.Stats
|
7, // 1: resource.ResourceStatsResponse.stats:type_name -> resource.ResourceStatsResponse.Stats
|
||||||
14, // 2: resource.ResourceSearchRequest.options:type_name -> resource.ListOptions
|
16, // 2: resource.ResourceSearchRequest.options:type_name -> resource.ListOptions
|
||||||
15, // 3: resource.ResourceSearchRequest.federated:type_name -> resource.ResourceKey
|
17, // 3: resource.ResourceSearchRequest.federated:type_name -> resource.ResourceKey
|
||||||
7, // 4: resource.ResourceSearchRequest.sortBy:type_name -> resource.ResourceSearchRequest.Sort
|
8, // 4: resource.ResourceSearchRequest.sortBy:type_name -> resource.ResourceSearchRequest.Sort
|
||||||
9, // 5: resource.ResourceSearchRequest.facet:type_name -> resource.ResourceSearchRequest.FacetEntry
|
11, // 5: resource.ResourceSearchRequest.facet:type_name -> resource.ResourceSearchRequest.FacetEntry
|
||||||
13, // 6: resource.ResourceSearchResponse.error:type_name -> resource.ErrorResult
|
10, // 6: resource.ResourceSearchRequest.query_fields:type_name -> resource.ResourceSearchRequest.QueryField
|
||||||
15, // 7: resource.ResourceSearchResponse.key:type_name -> resource.ResourceKey
|
15, // 7: resource.ResourceSearchResponse.error:type_name -> resource.ErrorResult
|
||||||
16, // 8: resource.ResourceSearchResponse.results:type_name -> resource.ResourceTable
|
17, // 8: resource.ResourceSearchResponse.key:type_name -> resource.ResourceKey
|
||||||
12, // 9: resource.ResourceSearchResponse.facet:type_name -> resource.ResourceSearchResponse.FacetEntry
|
18, // 9: resource.ResourceSearchResponse.results:type_name -> resource.ResourceTable
|
||||||
15, // 10: resource.RebuildIndexesRequest.keys:type_name -> resource.ResourceKey
|
14, // 10: resource.ResourceSearchResponse.facet:type_name -> resource.ResourceSearchResponse.FacetEntry
|
||||||
13, // 11: resource.RebuildIndexesResponse.error:type_name -> resource.ErrorResult
|
17, // 11: resource.RebuildIndexesRequest.keys:type_name -> resource.ResourceKey
|
||||||
8, // 12: resource.ResourceSearchRequest.FacetEntry.value:type_name -> resource.ResourceSearchRequest.Facet
|
15, // 12: resource.RebuildIndexesResponse.error:type_name -> resource.ErrorResult
|
||||||
11, // 13: resource.ResourceSearchResponse.Facet.terms:type_name -> resource.ResourceSearchResponse.TermFacet
|
0, // 13: resource.ResourceSearchRequest.QueryField.type:type_name -> resource.QueryFieldType
|
||||||
10, // 14: resource.ResourceSearchResponse.FacetEntry.value:type_name -> resource.ResourceSearchResponse.Facet
|
9, // 14: resource.ResourceSearchRequest.FacetEntry.value:type_name -> resource.ResourceSearchRequest.Facet
|
||||||
2, // 15: resource.ResourceIndex.Search:input_type -> resource.ResourceSearchRequest
|
13, // 15: resource.ResourceSearchResponse.Facet.terms:type_name -> resource.ResourceSearchResponse.TermFacet
|
||||||
0, // 16: resource.ResourceIndex.GetStats:input_type -> resource.ResourceStatsRequest
|
12, // 16: resource.ResourceSearchResponse.FacetEntry.value:type_name -> resource.ResourceSearchResponse.Facet
|
||||||
4, // 17: resource.ResourceIndex.RebuildIndexes:input_type -> resource.RebuildIndexesRequest
|
3, // 17: resource.ResourceIndex.Search:input_type -> resource.ResourceSearchRequest
|
||||||
3, // 18: resource.ResourceIndex.Search:output_type -> resource.ResourceSearchResponse
|
1, // 18: resource.ResourceIndex.GetStats:input_type -> resource.ResourceStatsRequest
|
||||||
1, // 19: resource.ResourceIndex.GetStats:output_type -> resource.ResourceStatsResponse
|
5, // 19: resource.ResourceIndex.RebuildIndexes:input_type -> resource.RebuildIndexesRequest
|
||||||
5, // 20: resource.ResourceIndex.RebuildIndexes:output_type -> resource.RebuildIndexesResponse
|
4, // 20: resource.ResourceIndex.Search:output_type -> resource.ResourceSearchResponse
|
||||||
18, // [18:21] is the sub-list for method output_type
|
2, // 21: resource.ResourceIndex.GetStats:output_type -> resource.ResourceStatsResponse
|
||||||
15, // [15:18] is the sub-list for method input_type
|
6, // 22: resource.ResourceIndex.RebuildIndexes:output_type -> resource.RebuildIndexesResponse
|
||||||
15, // [15:15] is the sub-list for extension type_name
|
20, // [20:23] is the sub-list for method output_type
|
||||||
15, // [15:15] is the sub-list for extension extendee
|
17, // [17:20] is the sub-list for method input_type
|
||||||
0, // [0:15] is the sub-list for field type_name
|
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() }
|
func init() { file_search_proto_init() }
|
||||||
@@ -1007,13 +1160,14 @@ func file_search_proto_init() {
|
|||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_search_proto_rawDesc), len(file_search_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_search_proto_rawDesc), len(file_search_proto_rawDesc)),
|
||||||
NumEnums: 0,
|
NumEnums: 1,
|
||||||
NumMessages: 13,
|
NumMessages: 14,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
GoTypes: file_search_proto_goTypes,
|
GoTypes: file_search_proto_goTypes,
|
||||||
DependencyIndexes: file_search_proto_depIdxs,
|
DependencyIndexes: file_search_proto_depIdxs,
|
||||||
|
EnumInfos: file_search_proto_enumTypes,
|
||||||
MessageInfos: file_search_proto_msgTypes,
|
MessageInfos: file_search_proto_msgTypes,
|
||||||
}.Build()
|
}.Build()
|
||||||
File_search_proto = out.File
|
File_search_proto = out.File
|
||||||
|
|||||||
@@ -31,9 +31,11 @@ const (
|
|||||||
// Unlike the ResourceStore, this service can be exposed to clients directly
|
// 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
|
// It should be implemented with efficient indexes and does not need read-after-write semantics
|
||||||
type ResourceIndexClient interface {
|
type ResourceIndexClient interface {
|
||||||
|
// Query for documents
|
||||||
Search(ctx context.Context, in *ResourceSearchRequest, opts ...grpc.CallOption) (*ResourceSearchResponse, error)
|
Search(ctx context.Context, in *ResourceSearchRequest, opts ...grpc.CallOption) (*ResourceSearchResponse, error)
|
||||||
// Get the resource stats
|
// Get the resource stats
|
||||||
GetStats(ctx context.Context, in *ResourceStatsRequest, opts ...grpc.CallOption) (*ResourceStatsResponse, error)
|
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)
|
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
|
// 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
|
// It should be implemented with efficient indexes and does not need read-after-write semantics
|
||||||
type ResourceIndexServer interface {
|
type ResourceIndexServer interface {
|
||||||
|
// Query for documents
|
||||||
Search(context.Context, *ResourceSearchRequest) (*ResourceSearchResponse, error)
|
Search(context.Context, *ResourceSearchRequest) (*ResourceSearchResponse, error)
|
||||||
// Get the resource stats
|
// Get the resource stats
|
||||||
GetStats(context.Context, *ResourceStatsRequest) (*ResourceStatsResponse, error)
|
GetStats(context.Context, *ResourceStatsRequest) (*ResourceStatsResponse, error)
|
||||||
|
// Rebuild the search index
|
||||||
RebuildIndexes(context.Context, *RebuildIndexesRequest) (*RebuildIndexesResponse, error)
|
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.
|
// Indexes that are not owned by current instance are eligible for cleanup.
|
||||||
// If nil, all indexes are owned by the current instance.
|
// If nil, all indexes are owned by the current instance.
|
||||||
OwnsIndex func(key resource.NamespacedResource) (bool, error)
|
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 {
|
type bleveBackend struct {
|
||||||
@@ -368,7 +373,7 @@ func (b *bleveBackend) BuildIndex(
|
|||||||
attribute.String("reason", indexBuildReason),
|
attribute.String("reason", indexBuildReason),
|
||||||
)
|
)
|
||||||
|
|
||||||
mapper, err := GetBleveMappings(fields)
|
mapper, err := GetBleveMappings(b.opts.ScoringModel, fields)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1177,6 +1182,7 @@ func (b *bleveIndex) getIndex(
|
|||||||
return b.index, nil
|
return b.index, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint:gocyclo
|
||||||
func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resourcepb.ResourceSearchRequest, access authlib.AccessClient) (*bleve.SearchRequest, *resourcepb.ErrorResult) {
|
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")
|
ctx, span := tracer.Start(ctx, "search.bleveIndex.toBleveSearchRequest")
|
||||||
defer span.End()
|
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, "*") {
|
if len(req.Query) > 1 {
|
||||||
// wildcard query is expensive - should be used with caution
|
if strings.Contains(req.Query, "*") {
|
||||||
wildcard := bleve.NewWildcardQuery(req.Query)
|
// wildcard query is expensive - should be used with caution
|
||||||
queries = append(queries, wildcard)
|
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, "*") {
|
queryFields := req.QueryFields
|
||||||
// Add a text query
|
if len(queryFields) == 0 {
|
||||||
searchrequest.Fields = append(searchrequest.Fields, resource.SEARCH_FIELD_SCORE)
|
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
|
case resourcepb.QueryFieldType_KEYWORD:
|
||||||
queryExact := bleve.NewMatchQuery(req.Query)
|
q := bleve.NewMatchQuery(req.Query)
|
||||||
queryExact.SetBoost(10.0)
|
q.SetBoost(float64(field.Boost))
|
||||||
queryExact.SetField(resource.SEARCH_FIELD_TITLE)
|
q.SetField(field.Name)
|
||||||
queryExact.Analyzer = keyword.Name // don't analyze the query input - treat it as a single token
|
q.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.
|
disjoin.AddQuery(q)
|
||||||
searchQuery := bleve.NewDisjunctionQuery(queryExact)
|
|
||||||
|
|
||||||
// Query 2: Phrase query with standard analyzer
|
case resourcepb.QueryFieldType_PHRASE:
|
||||||
queryPhrase := bleve.NewMatchPhraseQuery(req.Query)
|
q := bleve.NewMatchPhraseQuery(req.Query)
|
||||||
queryPhrase.SetBoost(5.0)
|
q.SetBoost(float64(field.Boost))
|
||||||
queryPhrase.SetField(resource.SEARCH_FIELD_TITLE)
|
q.SetField(field.Name)
|
||||||
queryPhrase.Analyzer = standard.Name
|
q.Analyzer = standard.Name
|
||||||
searchQuery.AddQuery(queryPhrase)
|
disjoin.AddQuery(q)
|
||||||
|
}
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch len(queries) {
|
switch len(queries) {
|
||||||
@@ -1872,7 +1898,7 @@ func (q *permissionScopedQuery) Searcher(ctx context.Context, i index.IndexReade
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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>
|
// 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
|
// 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
|
// index it becomes a binary encoded number that has some other internal meaning. Using ExternalID() will get the
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
index "github.com/blevesearch/bleve_index_api"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||||
@@ -19,6 +20,7 @@ func TestBleveSearchBackend(t *testing.T) {
|
|||||||
backend, err := NewBleveBackend(BleveOptions{
|
backend, err := NewBleveBackend(BleveOptions{
|
||||||
Root: tempDir,
|
Root: tempDir,
|
||||||
FileThreshold: 5,
|
FileThreshold: 5,
|
||||||
|
ScoringModel: index.BM25Scoring,
|
||||||
}, nil)
|
}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, backend)
|
require.NotNil(t, backend)
|
||||||
@@ -52,3 +54,32 @@ func TestSearchBackendBenchmark(t *testing.T) {
|
|||||||
|
|
||||||
unitest.BenchmarkSearchBackend(t, backend, opts)
|
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/keyword"
|
||||||
"github.com/blevesearch/bleve/v2/analysis/analyzer/standard"
|
"github.com/blevesearch/bleve/v2/analysis/analyzer/standard"
|
||||||
"github.com/blevesearch/bleve/v2/mapping"
|
"github.com/blevesearch/bleve/v2/mapping"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
"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()
|
mapper := bleve.NewIndexMapping()
|
||||||
|
if scoringModel != "" {
|
||||||
|
mapper.ScoringModel = scoringModel
|
||||||
|
}
|
||||||
|
|
||||||
err := RegisterCustomAnalyzers(mapper)
|
err := RegisterCustomAnalyzers(mapper)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestDocumentMapping(t *testing.T) {
|
func TestDocumentMapping(t *testing.T) {
|
||||||
mappings, err := search.GetBleveMappings(nil)
|
mappings, err := search.GetBleveMappings("", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
data := resource.IndexableDocument{
|
data := resource.IndexableDocument{
|
||||||
Title: "title",
|
Title: "title",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/blevesearch/bleve/v2"
|
"github.com/blevesearch/bleve/v2"
|
||||||
|
index "github.com/blevesearch/bleve_index_api"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
"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{
|
backend, err := search.NewBleveBackend(search.BleveOptions{
|
||||||
Root: t.TempDir(),
|
Root: t.TempDir(),
|
||||||
FileThreshold: threshold, // use in-memory for tests
|
FileThreshold: threshold, // use in-memory for tests
|
||||||
|
ScoringModel: index.BM25Scoring,
|
||||||
}, nil)
|
}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/blevesearch/bleve/v2"
|
"github.com/blevesearch/bleve/v2"
|
||||||
|
index "github.com/blevesearch/bleve_index_api"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -50,6 +51,7 @@ func TestBleveBackend(t *testing.T) {
|
|||||||
backend, err := NewBleveBackend(BleveOptions{
|
backend, err := NewBleveBackend(BleveOptions{
|
||||||
Root: tmpdir,
|
Root: tmpdir,
|
||||||
FileThreshold: 5, // with more than 5 items we create a file on disk
|
FileThreshold: 5, // with more than 5 items we create a file on disk
|
||||||
|
ScoringModel: index.BM25Scoring,
|
||||||
}, nil)
|
}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(backend.Stop)
|
t.Cleanup(backend.Stop)
|
||||||
@@ -773,6 +775,7 @@ func setupBleveBackend(t *testing.T, options ...setupOption) (*bleveBackend, pro
|
|||||||
IndexCacheTTL: defaultIndexCacheTTL,
|
IndexCacheTTL: defaultIndexCacheTTL,
|
||||||
Logger: log.NewNopLogger(),
|
Logger: log.NewNopLogger(),
|
||||||
BuildVersion: buildVersion,
|
BuildVersion: buildVersion,
|
||||||
|
ScoringModel: index.BM25Scoring,
|
||||||
}
|
}
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
opt(&opts)
|
opt(&opts)
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ func NewSearchOptions(
|
|||||||
BuildVersion: cfg.BuildVersion,
|
BuildVersion: cfg.BuildVersion,
|
||||||
OwnsIndex: ownsIndexFn,
|
OwnsIndex: ownsIndexFn,
|
||||||
IndexMinUpdateInterval: cfg.IndexMinUpdateInterval,
|
IndexMinUpdateInterval: cfg.IndexMinUpdateInterval,
|
||||||
|
ScoringModel: cfg.IndexScoringModel,
|
||||||
}, indexMetrics)
|
}, indexMetrics)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
index "github.com/blevesearch/bleve_index_api"
|
||||||
"github.com/go-jose/go-jose/v4/jwt"
|
"github.com/go-jose/go-jose/v4/jwt"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -129,21 +130,28 @@ func TestIntegrationSearchAndStorage(t *testing.T) {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Create a new bleve backend
|
scoringModels := []string{index.TFIDFScoring, index.BM25Scoring}
|
||||||
search, err := search.NewBleveBackend(search.BleveOptions{
|
|
||||||
FileThreshold: 0,
|
|
||||||
Root: t.TempDir(),
|
|
||||||
}, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, search)
|
|
||||||
t.Cleanup(search.Stop)
|
|
||||||
|
|
||||||
// Create a new resource backend
|
for _, model := range scoringModels {
|
||||||
storage, _ := newTestBackend(t, false, 0)
|
t.Run(model, func(t *testing.T) {
|
||||||
require.NotNil(t, storage)
|
// 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
|
// Create a new resource backend
|
||||||
unitest.RunTestSearchAndStorage(t, ctx, storage, search)
|
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) {
|
func TestClientServer(t *testing.T) {
|
||||||
|
|||||||
@@ -24,6 +24,24 @@ func TestMain(m *testing.M) {
|
|||||||
testsuite.Run(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) {
|
func TestIntegrationElasticsearch(t *testing.T) {
|
||||||
testutil.SkipIntegrationTestInShortMode(t)
|
testutil.SkipIntegrationTestInShortMode(t)
|
||||||
|
|
||||||
@@ -35,9 +53,8 @@ func TestIntegrationElasticsearch(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
var outgoingRequest *http.Request
|
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
|
outgoingRequest = r
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
}))
|
}))
|
||||||
t.Cleanup(outgoingServer.Close)
|
t.Cleanup(outgoingServer.Close)
|
||||||
|
|
||||||
|
|||||||
@@ -209,7 +209,7 @@
|
|||||||
"path": "public/plugins/grafana-azure-monitor-datasource/img/azure_monitor_cpu.png"
|
"path": "public/plugins/grafana-azure-monitor-datasource/img/azure_monitor_cpu.png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version": "12.3.0-pre",
|
"version": "12.4.0-pre",
|
||||||
"updated": "",
|
"updated": "",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"azure",
|
"azure",
|
||||||
@@ -589,7 +589,7 @@
|
|||||||
"hasUpdate": false,
|
"hasUpdate": false,
|
||||||
"defaultNavUrl": "/plugins/datagrid/",
|
"defaultNavUrl": "/plugins/datagrid/",
|
||||||
"category": "",
|
"category": "",
|
||||||
"state": "beta",
|
"state": "deprecated",
|
||||||
"signature": "internal",
|
"signature": "internal",
|
||||||
"signatureType": "",
|
"signatureType": "",
|
||||||
"signatureOrg": "",
|
"signatureOrg": "",
|
||||||
@@ -639,7 +639,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": "",
|
"grafanaDependency": "\u003e=11.6.0",
|
||||||
"grafanaVersion": "*",
|
"grafanaVersion": "*",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"extensions": {
|
"extensions": {
|
||||||
@@ -880,7 +880,7 @@
|
|||||||
},
|
},
|
||||||
"build": {},
|
"build": {},
|
||||||
"screenshots": null,
|
"screenshots": null,
|
||||||
"version": "12.3.0-pre",
|
"version": "12.4.0-pre",
|
||||||
"updated": "",
|
"updated": "",
|
||||||
"keywords": null
|
"keywords": null
|
||||||
},
|
},
|
||||||
@@ -934,7 +934,7 @@
|
|||||||
},
|
},
|
||||||
"build": {},
|
"build": {},
|
||||||
"screenshots": null,
|
"screenshots": null,
|
||||||
"version": "12.3.0-pre",
|
"version": "12.4.0-pre",
|
||||||
"updated": "",
|
"updated": "",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"grafana",
|
"grafana",
|
||||||
@@ -1000,7 +1000,7 @@
|
|||||||
},
|
},
|
||||||
"build": {},
|
"build": {},
|
||||||
"screenshots": null,
|
"screenshots": null,
|
||||||
"version": "12.3.0-pre",
|
"version": "12.4.0-pre",
|
||||||
"updated": "",
|
"updated": "",
|
||||||
"keywords": null
|
"keywords": null
|
||||||
},
|
},
|
||||||
@@ -1217,7 +1217,7 @@
|
|||||||
},
|
},
|
||||||
"build": {},
|
"build": {},
|
||||||
"screenshots": null,
|
"screenshots": null,
|
||||||
"version": "12.3.0-pre",
|
"version": "12.4.0-pre",
|
||||||
"updated": "",
|
"updated": "",
|
||||||
"keywords": null
|
"keywords": null
|
||||||
},
|
},
|
||||||
@@ -1325,7 +1325,7 @@
|
|||||||
},
|
},
|
||||||
"build": {},
|
"build": {},
|
||||||
"screenshots": null,
|
"screenshots": null,
|
||||||
"version": "12.3.0-pre",
|
"version": "12.4.0-pre",
|
||||||
"updated": "",
|
"updated": "",
|
||||||
"keywords": null
|
"keywords": null
|
||||||
},
|
},
|
||||||
@@ -1375,7 +1375,7 @@
|
|||||||
},
|
},
|
||||||
"build": {},
|
"build": {},
|
||||||
"screenshots": null,
|
"screenshots": null,
|
||||||
"version": "12.3.0-pre",
|
"version": "12.4.0-pre",
|
||||||
"updated": "",
|
"updated": "",
|
||||||
"keywords": null
|
"keywords": null
|
||||||
},
|
},
|
||||||
@@ -1425,7 +1425,7 @@
|
|||||||
},
|
},
|
||||||
"build": {},
|
"build": {},
|
||||||
"screenshots": null,
|
"screenshots": null,
|
||||||
"version": "12.3.0-pre",
|
"version": "12.4.0-pre",
|
||||||
"updated": "",
|
"updated": "",
|
||||||
"keywords": null
|
"keywords": null
|
||||||
},
|
},
|
||||||
@@ -1575,7 +1575,7 @@
|
|||||||
},
|
},
|
||||||
"build": {},
|
"build": {},
|
||||||
"screenshots": null,
|
"screenshots": null,
|
||||||
"version": "",
|
"version": "12.4.0-pre",
|
||||||
"updated": "",
|
"updated": "",
|
||||||
"keywords": null
|
"keywords": null
|
||||||
},
|
},
|
||||||
@@ -1629,7 +1629,7 @@
|
|||||||
},
|
},
|
||||||
"build": {},
|
"build": {},
|
||||||
"screenshots": null,
|
"screenshots": null,
|
||||||
"version": "12.3.0-pre",
|
"version": "12.4.0-pre",
|
||||||
"updated": "",
|
"updated": "",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"grafana",
|
"grafana",
|
||||||
@@ -1734,7 +1734,7 @@
|
|||||||
},
|
},
|
||||||
"build": {},
|
"build": {},
|
||||||
"screenshots": null,
|
"screenshots": null,
|
||||||
"version": "12.3.0-pre",
|
"version": "12.4.0-pre",
|
||||||
"updated": "",
|
"updated": "",
|
||||||
"keywords": null
|
"keywords": null
|
||||||
},
|
},
|
||||||
@@ -2042,7 +2042,7 @@
|
|||||||
},
|
},
|
||||||
"build": {},
|
"build": {},
|
||||||
"screenshots": null,
|
"screenshots": null,
|
||||||
"version": "12.3.0-pre",
|
"version": "12.4.0-pre",
|
||||||
"updated": "",
|
"updated": "",
|
||||||
"keywords": null
|
"keywords": null
|
||||||
},
|
},
|
||||||
@@ -2092,7 +2092,7 @@
|
|||||||
},
|
},
|
||||||
"build": {},
|
"build": {},
|
||||||
"screenshots": null,
|
"screenshots": null,
|
||||||
"version": "12.3.0-pre",
|
"version": "12.4.0-pre",
|
||||||
"updated": "",
|
"updated": "",
|
||||||
"keywords": null
|
"keywords": null
|
||||||
},
|
},
|
||||||
@@ -2445,7 +2445,7 @@
|
|||||||
},
|
},
|
||||||
"build": {},
|
"build": {},
|
||||||
"screenshots": null,
|
"screenshots": null,
|
||||||
"version": "12.3.0-pre",
|
"version": "12.4.0-pre",
|
||||||
"updated": "",
|
"updated": "",
|
||||||
"keywords": null
|
"keywords": null
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func TestIntegrationSearchDevDashboards(t *testing.T) {
|
|||||||
require.Equal(t, 16, fileCount, "file count from %s", devenv)
|
require.Equal(t, 16, fileCount, "file count from %s", devenv)
|
||||||
|
|
||||||
// Helper to call search
|
// 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)
|
require.NotNil(t, user)
|
||||||
ns := user.Identity.GetNamespace()
|
ns := user.Identity.GetNamespace()
|
||||||
cfg := dynamic.ConfigFor(user.NewRestConfig())
|
cfg := dynamic.ConfigFor(user.NewRestConfig())
|
||||||
@@ -107,17 +107,12 @@ func TestIntegrationSearchDevDashboards(t *testing.T) {
|
|||||||
|
|
||||||
var statusCode int
|
var statusCode int
|
||||||
req := restClient.Get().AbsPath("apis", "dashboard.grafana.app", "v0alpha1", "namespaces", ns, "search").
|
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("limit", "1000").
|
||||||
Param("type", "dashboard") // Only search dashboards
|
Param("type", "dashboard") // Only search dashboards
|
||||||
|
|
||||||
for kv := range strings.SplitSeq(params, "&") {
|
for k, v := range params {
|
||||||
if kv == "" {
|
req = req.Param(k, v)
|
||||||
continue
|
|
||||||
}
|
|
||||||
parts := strings.SplitN(kv, "=", 2)
|
|
||||||
if len(parts) == 2 {
|
|
||||||
req = req.Param(parts[0], parts[1])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
res := req.Do(ctx).StatusCode(&statusCode)
|
res := req.Do(ctx).StatusCode(&statusCode)
|
||||||
require.NoError(t, res.Error())
|
require.NoError(t, res.Error())
|
||||||
@@ -140,22 +135,47 @@ func TestIntegrationSearchDevDashboards(t *testing.T) {
|
|||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
user apis.User
|
user apis.User
|
||||||
params string
|
params map[string]string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "all",
|
name: "all",
|
||||||
user: helper.Org1.Admin,
|
user: helper.Org1.Admin,
|
||||||
params: "", // only dashboards
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "simple-query",
|
name: "query-single-word",
|
||||||
user: helper.Org1.Admin,
|
user: helper.Org1.Admin,
|
||||||
params: "query=stacking",
|
params: map[string]string{
|
||||||
|
"query": "stacking",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with-text-panel",
|
name: "query-multiple-words",
|
||||||
user: helper.Org1.Admin,
|
user: helper.Org1.Admin,
|
||||||
params: "field=panel_types&panelType=text",
|
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 {
|
for i, tc := range testCases {
|
||||||
|
|||||||
+3
-3
@@ -10,7 +10,7 @@
|
|||||||
"panel-tests",
|
"panel-tests",
|
||||||
"graph-ng"
|
"graph-ng"
|
||||||
],
|
],
|
||||||
"score": 0.658
|
"score": 0.284
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resource": "dashboards",
|
"resource": "dashboards",
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
"panel-tests",
|
"panel-tests",
|
||||||
"graph-ng"
|
"graph-ng"
|
||||||
],
|
],
|
||||||
"score": 0.625
|
"score": 0.269
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"maxScore": 0.658
|
"maxScore": 0.284
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
}, 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)
|
request, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -139,6 +139,7 @@ type datasourceInfo struct {
|
|||||||
defaultProject string
|
defaultProject string
|
||||||
clientEmail string
|
clientEmail string
|
||||||
tokenUri string
|
tokenUri string
|
||||||
|
universeDomain string
|
||||||
services map[string]datasourceService
|
services map[string]datasourceService
|
||||||
privateKey string
|
privateKey string
|
||||||
usingImpersonation bool
|
usingImpersonation bool
|
||||||
@@ -150,6 +151,7 @@ type datasourceJSONData struct {
|
|||||||
DefaultProject string `json:"defaultProject"`
|
DefaultProject string `json:"defaultProject"`
|
||||||
ClientEmail string `json:"clientEmail"`
|
ClientEmail string `json:"clientEmail"`
|
||||||
TokenURI string `json:"tokenUri"`
|
TokenURI string `json:"tokenUri"`
|
||||||
|
UniverseDomain string `json:"universeDomain"`
|
||||||
UsingImpersonation bool `json:"usingImpersonation"`
|
UsingImpersonation bool `json:"usingImpersonation"`
|
||||||
ServiceAccountToImpersonate string `json:"serviceAccountToImpersonate"`
|
ServiceAccountToImpersonate string `json:"serviceAccountToImpersonate"`
|
||||||
}
|
}
|
||||||
@@ -179,6 +181,7 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
|
|||||||
defaultProject: jsonData.DefaultProject,
|
defaultProject: jsonData.DefaultProject,
|
||||||
clientEmail: jsonData.ClientEmail,
|
clientEmail: jsonData.ClientEmail,
|
||||||
tokenUri: jsonData.TokenURI,
|
tokenUri: jsonData.TokenURI,
|
||||||
|
universeDomain: jsonData.UniverseDomain,
|
||||||
usingImpersonation: jsonData.UsingImpersonation,
|
usingImpersonation: jsonData.UsingImpersonation,
|
||||||
serviceAccountToImpersonate: jsonData.ServiceAccountToImpersonate,
|
serviceAccountToImpersonate: jsonData.ServiceAccountToImpersonate,
|
||||||
services: map[string]datasourceService{},
|
services: map[string]datasourceService{},
|
||||||
@@ -194,13 +197,13 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, info := range routes {
|
for name := range routes {
|
||||||
client, err := newHTTPClient(dsInfo, opts, &httpClientProvider, name)
|
client, err := newHTTPClient(dsInfo, opts, &httpClientProvider, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
dsInfo.services[name] = datasourceService{
|
dsInfo.services[name] = datasourceService{
|
||||||
url: info.url,
|
url: buildURL(name, dsInfo.universeDomain),
|
||||||
client: client,
|
client: client,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ type routeInfo struct {
|
|||||||
var routes = map[string]routeInfo{
|
var routes = map[string]routeInfo{
|
||||||
cloudMonitor: {
|
cloudMonitor: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "https://monitoring.googleapis.com",
|
url: "https://monitoring.",
|
||||||
scopes: []string{cloudMonitorScope},
|
scopes: []string{cloudMonitorScope},
|
||||||
},
|
},
|
||||||
resourceManager: {
|
resourceManager: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "https://cloudresourcemanager.googleapis.com",
|
url: "https://cloudresourcemanager.",
|
||||||
scopes: []string{resourceManagerScope},
|
scopes: []string{resourceManagerScope},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -68,6 +68,13 @@ func getMiddleware(model *datasourceInfo, routePath string) (httpclient.Middlewa
|
|||||||
return tokenprovider.AuthMiddleware(provider), nil
|
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) {
|
func newHTTPClient(model *datasourceInfo, opts httpclient.Options, clientProvider *httpclient.Provider, route string) (*http.Client, error) {
|
||||||
m, err := getMiddleware(model, route)
|
m, err := getMiddleware(model, route)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ func Test_setRequestVariables(t *testing.T) {
|
|||||||
im: &fakeInstance{
|
im: &fakeInstance{
|
||||||
services: map[string]datasourceService{
|
services: map[string]datasourceService{
|
||||||
cloudMonitor: {
|
cloudMonitor: {
|
||||||
url: routes[cloudMonitor].url,
|
url: buildURL(cloudMonitor, "googleapis.com"),
|
||||||
client: &http.Client{},
|
client: &http.Client{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package elasticsearch
|
|||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
||||||
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
|
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
|
// 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"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
|
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Used in logging to mark a stage
|
// Used in logging to mark a stage
|
||||||
@@ -35,6 +34,7 @@ type DatasourceInfo struct {
|
|||||||
Interval string
|
Interval string
|
||||||
MaxConcurrentShardRequests int64
|
MaxConcurrentShardRequests int64
|
||||||
IncludeFrozen bool
|
IncludeFrozen bool
|
||||||
|
ClusterInfo ClusterInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfiguredFields struct {
|
type ConfiguredFields struct {
|
||||||
@@ -159,7 +159,7 @@ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearch
|
|||||||
resSpan.End()
|
resSpan.End()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
improvedParsingEnabled := isFeatureEnabled(c.ctx, featuremgmt.FlagElasticsearchImprovedParsing)
|
improvedParsingEnabled := isFeatureEnabled(c.ctx, "elasticsearchImprovedParsing")
|
||||||
msr, err := c.parser.parseMultiSearchResponse(res.Body, improvedParsingEnabled)
|
msr, err := c.parser.parseMultiSearchResponse(res.Body, improvedParsingEnabled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -197,7 +197,11 @@ func (c *baseClientImpl) createMultiSearchRequests(searchRequests []*SearchReque
|
|||||||
|
|
||||||
func (c *baseClientImpl) getMultiSearchQueryParameters() string {
|
func (c *baseClientImpl) getMultiSearchQueryParameters() string {
|
||||||
var qs []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 {
|
if c.ds.IncludeFrozen {
|
||||||
qs = append(qs, "ignore_throttled=false")
|
qs = append(qs, "ignore_throttled=false")
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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) {
|
func TestClient_ExecuteMultisearch(t *testing.T) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"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) {
|
func TestSearchRequest(t *testing.T) {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
||||||
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
|
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
|
// processQuery processes a single query and adds it to the multi-search request builder
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package elasticsearch
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"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
|
// 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"
|
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)
|
httpCli, err := httpClientProvider.New(httpCliOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -151,6 +159,11 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins
|
|||||||
includeFrozen = false
|
includeFrozen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clusterInfo, err := es.GetClusterInfo(httpCli, settings.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
configuredFields := es.ConfiguredFields{
|
configuredFields := es.ConfiguredFields{
|
||||||
TimeField: timeField,
|
TimeField: timeField,
|
||||||
LogLevelField: logLevelField,
|
LogLevelField: logLevelField,
|
||||||
@@ -166,6 +179,7 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins
|
|||||||
ConfiguredFields: configuredFields,
|
ConfiguredFields: configuredFields,
|
||||||
Interval: interval,
|
Interval: interval,
|
||||||
IncludeFrozen: includeFrozen,
|
IncludeFrozen: includeFrozen,
|
||||||
|
ClusterInfo: clusterInfo,
|
||||||
}
|
}
|
||||||
return model, nil
|
return model, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package elasticsearch
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
@@ -18,8 +20,26 @@ type datasourceInfo struct {
|
|||||||
Interval string `json:"interval"`
|
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) {
|
func TestNewInstanceSettings(t *testing.T) {
|
||||||
t.Run("fields exist", func(t *testing.T) {
|
t.Run("fields exist", func(t *testing.T) {
|
||||||
|
server := mockElasticsearchServer()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
dsInfo := datasourceInfo{
|
dsInfo := datasourceInfo{
|
||||||
TimeField: "@timestamp",
|
TimeField: "@timestamp",
|
||||||
MaxConcurrentShardRequests: 5,
|
MaxConcurrentShardRequests: 5,
|
||||||
@@ -28,6 +48,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
dsSettings := backend.DataSourceInstanceSettings{
|
dsSettings := backend.DataSourceInstanceSettings{
|
||||||
|
URL: server.URL,
|
||||||
JSONData: json.RawMessage(settingsJSON),
|
JSONData: json.RawMessage(settingsJSON),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +58,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("timeField", func(t *testing.T) {
|
t.Run("timeField", func(t *testing.T) {
|
||||||
t.Run("is nil", func(t *testing.T) {
|
t.Run("is nil", func(t *testing.T) {
|
||||||
|
server := mockElasticsearchServer()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
dsInfo := datasourceInfo{
|
dsInfo := datasourceInfo{
|
||||||
MaxConcurrentShardRequests: 5,
|
MaxConcurrentShardRequests: 5,
|
||||||
Interval: "Daily",
|
Interval: "Daily",
|
||||||
@@ -46,6 +70,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
dsSettings := backend.DataSourceInstanceSettings{
|
dsSettings := backend.DataSourceInstanceSettings{
|
||||||
|
URL: server.URL,
|
||||||
JSONData: json.RawMessage(settingsJSON),
|
JSONData: json.RawMessage(settingsJSON),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +79,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("is empty", func(t *testing.T) {
|
t.Run("is empty", func(t *testing.T) {
|
||||||
|
server := mockElasticsearchServer()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
dsInfo := datasourceInfo{
|
dsInfo := datasourceInfo{
|
||||||
MaxConcurrentShardRequests: 5,
|
MaxConcurrentShardRequests: 5,
|
||||||
Interval: "Daily",
|
Interval: "Daily",
|
||||||
@@ -64,6 +92,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
dsSettings := backend.DataSourceInstanceSettings{
|
dsSettings := backend.DataSourceInstanceSettings{
|
||||||
|
URL: server.URL,
|
||||||
JSONData: json.RawMessage(settingsJSON),
|
JSONData: json.RawMessage(settingsJSON),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +103,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("maxConcurrentShardRequests", func(t *testing.T) {
|
t.Run("maxConcurrentShardRequests", func(t *testing.T) {
|
||||||
t.Run("no maxConcurrentShardRequests", func(t *testing.T) {
|
t.Run("no maxConcurrentShardRequests", func(t *testing.T) {
|
||||||
|
server := mockElasticsearchServer()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
dsInfo := datasourceInfo{
|
dsInfo := datasourceInfo{
|
||||||
TimeField: "@timestamp",
|
TimeField: "@timestamp",
|
||||||
}
|
}
|
||||||
@@ -81,6 +113,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
dsSettings := backend.DataSourceInstanceSettings{
|
dsSettings := backend.DataSourceInstanceSettings{
|
||||||
|
URL: server.URL,
|
||||||
JSONData: json.RawMessage(settingsJSON),
|
JSONData: json.RawMessage(settingsJSON),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +123,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("string maxConcurrentShardRequests", func(t *testing.T) {
|
t.Run("string maxConcurrentShardRequests", func(t *testing.T) {
|
||||||
|
server := mockElasticsearchServer()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
dsInfo := datasourceInfo{
|
dsInfo := datasourceInfo{
|
||||||
TimeField: "@timestamp",
|
TimeField: "@timestamp",
|
||||||
MaxConcurrentShardRequests: "10",
|
MaxConcurrentShardRequests: "10",
|
||||||
@@ -98,6 +134,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
dsSettings := backend.DataSourceInstanceSettings{
|
dsSettings := backend.DataSourceInstanceSettings{
|
||||||
|
URL: server.URL,
|
||||||
JSONData: json.RawMessage(settingsJSON),
|
JSONData: json.RawMessage(settingsJSON),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +144,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("number maxConcurrentShardRequests", func(t *testing.T) {
|
t.Run("number maxConcurrentShardRequests", func(t *testing.T) {
|
||||||
|
server := mockElasticsearchServer()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
dsInfo := datasourceInfo{
|
dsInfo := datasourceInfo{
|
||||||
TimeField: "@timestamp",
|
TimeField: "@timestamp",
|
||||||
MaxConcurrentShardRequests: 10,
|
MaxConcurrentShardRequests: 10,
|
||||||
@@ -115,6 +155,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
dsSettings := backend.DataSourceInstanceSettings{
|
dsSettings := backend.DataSourceInstanceSettings{
|
||||||
|
URL: server.URL,
|
||||||
JSONData: json.RawMessage(settingsJSON),
|
JSONData: json.RawMessage(settingsJSON),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +165,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("zero maxConcurrentShardRequests", func(t *testing.T) {
|
t.Run("zero maxConcurrentShardRequests", func(t *testing.T) {
|
||||||
|
server := mockElasticsearchServer()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
dsInfo := datasourceInfo{
|
dsInfo := datasourceInfo{
|
||||||
TimeField: "@timestamp",
|
TimeField: "@timestamp",
|
||||||
MaxConcurrentShardRequests: 0,
|
MaxConcurrentShardRequests: 0,
|
||||||
@@ -132,6 +176,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
dsSettings := backend.DataSourceInstanceSettings{
|
dsSettings := backend.DataSourceInstanceSettings{
|
||||||
|
URL: server.URL,
|
||||||
JSONData: json.RawMessage(settingsJSON),
|
JSONData: json.RawMessage(settingsJSON),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +186,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("negative maxConcurrentShardRequests", func(t *testing.T) {
|
t.Run("negative maxConcurrentShardRequests", func(t *testing.T) {
|
||||||
|
server := mockElasticsearchServer()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
dsInfo := datasourceInfo{
|
dsInfo := datasourceInfo{
|
||||||
TimeField: "@timestamp",
|
TimeField: "@timestamp",
|
||||||
MaxConcurrentShardRequests: -10,
|
MaxConcurrentShardRequests: -10,
|
||||||
@@ -149,6 +197,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
dsSettings := backend.DataSourceInstanceSettings{
|
dsSettings := backend.DataSourceInstanceSettings{
|
||||||
|
URL: server.URL,
|
||||||
JSONData: json.RawMessage(settingsJSON),
|
JSONData: json.RawMessage(settingsJSON),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +207,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("float maxConcurrentShardRequests", func(t *testing.T) {
|
t.Run("float maxConcurrentShardRequests", func(t *testing.T) {
|
||||||
|
server := mockElasticsearchServer()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
dsInfo := datasourceInfo{
|
dsInfo := datasourceInfo{
|
||||||
TimeField: "@timestamp",
|
TimeField: "@timestamp",
|
||||||
MaxConcurrentShardRequests: 10.5,
|
MaxConcurrentShardRequests: 10.5,
|
||||||
@@ -166,6 +218,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
dsSettings := backend.DataSourceInstanceSettings{
|
dsSettings := backend.DataSourceInstanceSettings{
|
||||||
|
URL: server.URL,
|
||||||
JSONData: json.RawMessage(settingsJSON),
|
JSONData: json.RawMessage(settingsJSON),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +228,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("invalid maxConcurrentShardRequests", func(t *testing.T) {
|
t.Run("invalid maxConcurrentShardRequests", func(t *testing.T) {
|
||||||
|
server := mockElasticsearchServer()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
dsInfo := datasourceInfo{
|
dsInfo := datasourceInfo{
|
||||||
TimeField: "@timestamp",
|
TimeField: "@timestamp",
|
||||||
MaxConcurrentShardRequests: "invalid",
|
MaxConcurrentShardRequests: "invalid",
|
||||||
@@ -183,6 +239,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
dsSettings := backend.DataSourceInstanceSettings{
|
dsSettings := backend.DataSourceInstanceSettings{
|
||||||
|
URL: server.URL,
|
||||||
JSONData: json.RawMessage(settingsJSON),
|
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",
|
Message: "Health check failed: Failed to get data source info",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
healthStatusUrl, err := url.Parse(ds.URL)
|
healthStatusUrl, err := url.Parse(ds.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to parse data source URL", "error", err)
|
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
|
}, 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
|
// check that ES is healthy
|
||||||
healthStatusUrl.Path = path.Join(healthStatusUrl.Path, "_cluster/health")
|
healthStatusUrl.Path = path.Join(healthStatusUrl.Path, "_cluster/health")
|
||||||
healthStatusUrl.RawQuery = "wait_for_status=yellow"
|
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/backend"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"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
|
// 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