Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c558072c0c | |||
| 2ce89f099f | |||
| 829022d488 |
@@ -121,8 +121,6 @@ linters:
|
||||
- '**/pkg/tsdb/zipkin/**/*'
|
||||
- '**/pkg/tsdb/jaeger/*'
|
||||
- '**/pkg/tsdb/jaeger/**/*'
|
||||
- '**/pkg/tsdb/elasticsearch/*'
|
||||
- '**/pkg/tsdb/elasticsearch/**/*'
|
||||
deny:
|
||||
- pkg: github.com/grafana/grafana/pkg/api
|
||||
desc: Core plugins are not allowed to depend on Grafana core packages
|
||||
|
||||
@@ -800,8 +800,6 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -804,8 +804,6 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -241,8 +241,6 @@ lineage: schemas: [{
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
// Options to config when to refresh a variable
|
||||
|
||||
@@ -241,8 +241,6 @@ lineage: schemas: [{
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
// Options to config when to refresh a variable
|
||||
|
||||
@@ -804,8 +804,6 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -1426,8 +1426,6 @@ type DashboardVariableOption struct {
|
||||
Text DashboardStringOrArrayOfString `json:"text"`
|
||||
// Value of the option
|
||||
Value DashboardStringOrArrayOfString `json:"value"`
|
||||
// Additional properties for multi-props variables
|
||||
Properties map[string]string `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// NewDashboardVariableOption creates a new DashboardVariableOption object.
|
||||
|
||||
@@ -5133,22 +5133,6 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardVariableOption(ref common.Refer
|
||||
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStringOrArrayOfString"),
|
||||
},
|
||||
},
|
||||
"properties": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Additional properties for multi-props variables",
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"text", "value"},
|
||||
},
|
||||
|
||||
@@ -808,8 +808,6 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -1429,8 +1429,6 @@ type DashboardVariableOption struct {
|
||||
Text DashboardStringOrArrayOfString `json:"text"`
|
||||
// Value of the option
|
||||
Value DashboardStringOrArrayOfString `json:"value"`
|
||||
// Additional properties for multi-props variables
|
||||
Properties map[string]string `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// NewDashboardVariableOption creates a new DashboardVariableOption object.
|
||||
|
||||
@@ -5196,22 +5196,6 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardVariableOption(ref common.Refere
|
||||
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardStringOrArrayOfString"),
|
||||
},
|
||||
},
|
||||
"properties": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Additional properties for multi-props variables",
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"text", "value"},
|
||||
},
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
@@ -103,11 +103,10 @@ To configure basic settings for the data source, complete the following steps:
|
||||
|
||||
1. Set the data source's basic configuration options:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | Sets the name you use to refer to the data source in panels and queries. |
|
||||
| **Default** | Sets whether the data source is pre-selected for new panels. |
|
||||
| **Universe Domain** | The universe domain to connect to. For more information, refer to [Documentation on universe domains](https://docs.cloud.google.com/python/docs/reference/monitoring/latest/google.cloud.monitoring_v3.services.service_monitoring_service.ServiceMonitoringServiceAsyncClient#google_cloud_monitoring_v3_services_service_monitoring_service_ServiceMonitoringServiceAsyncClient_universe_domain). Defaults to `googleapis.com`. |
|
||||
| Name | Description |
|
||||
| ----------- | ------------------------------------------------------------------------ |
|
||||
| **Name** | Sets the name you use to refer to the data source in panels and queries. |
|
||||
| **Default** | Sets whether the data source is pre-selected for new panels. |
|
||||
|
||||
### Provision the data source
|
||||
|
||||
@@ -130,7 +129,6 @@ datasources:
|
||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||
authenticationType: jwt
|
||||
defaultProject: my-project-name
|
||||
universeDomain: googleapis.com
|
||||
secureJsonData:
|
||||
privateKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
@@ -154,7 +152,6 @@ datasources:
|
||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||
authenticationType: jwt
|
||||
defaultProject: my-project-name
|
||||
universeDomain: googleapis.com
|
||||
privateKeyPath: /etc/secrets/gce.pem
|
||||
```
|
||||
|
||||
@@ -169,7 +166,6 @@ datasources:
|
||||
access: proxy
|
||||
jsonData:
|
||||
authenticationType: gce
|
||||
universeDomain: googleapis.com
|
||||
```
|
||||
|
||||
## Import pre-configured dashboards
|
||||
|
||||
@@ -87,7 +87,6 @@ With a Grafana Enterprise license, you also get access to premium data sources,
|
||||
- [CockroachDB](/grafana/plugins/grafana-cockroachdb-datasource)
|
||||
- [Databricks](/grafana/plugins/grafana-databricks-datasource)
|
||||
- [DataDog](/grafana/plugins/grafana-datadog-datasource)
|
||||
- [IBM Db2](/grafana/plugins/grafana-ibmdb2-datasource)
|
||||
- [Drone](/grafana/plugins/grafana-drone-datasource)
|
||||
- [DynamoDB](/grafana/plugins/grafana-dynamodb-datasource/)
|
||||
- [Dynatrace](/grafana/plugins/grafana-dynatrace-datasource)
|
||||
|
||||
@@ -3743,21 +3743,46 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/aggregations.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/aggregations.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/configuration/DataLinks.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
|
||||
@@ -82,7 +82,6 @@ module.exports = {
|
||||
// Decoupled plugins run their own tests so ignoring them here.
|
||||
'<rootDir>/public/app/plugins/datasource/azuremonitor',
|
||||
'<rootDir>/public/app/plugins/datasource/cloud-monitoring',
|
||||
'<rootDir>/public/app/plugins/datasource/elasticsearch',
|
||||
'<rootDir>/public/app/plugins/datasource/grafana-postgresql-datasource',
|
||||
'<rootDir>/public/app/plugins/datasource/grafana-pyroscope-datasource',
|
||||
'<rootDir>/public/app/plugins/datasource/grafana-testdata-datasource',
|
||||
|
||||
@@ -237,8 +237,6 @@ lineage: schemas: [{
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
// Options to config when to refresh a variable
|
||||
|
||||
@@ -91,8 +91,6 @@ export interface VariableOption {
|
||||
text: string | string[];
|
||||
value: string | string[];
|
||||
isNone?: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
properties?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface IntervalVariableModel extends VariableWithOptions {
|
||||
@@ -120,7 +118,6 @@ export interface QueryVariableModel extends VariableWithMultiSupport {
|
||||
definition: string;
|
||||
sort: VariableSort;
|
||||
queryValue?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
query: any;
|
||||
regex: string;
|
||||
regexApplyTo?: VariableRegexApplyTo;
|
||||
@@ -196,7 +193,6 @@ export interface BaseVariableModel {
|
||||
skipUrlSync: boolean;
|
||||
index: number;
|
||||
state: LoadingState;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error: any | null;
|
||||
description: string | null;
|
||||
usedInRepeat?: boolean;
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
|
||||
import * as common from '@grafana/schema';
|
||||
|
||||
export const pluginVersion = "%VERSION%";
|
||||
export const pluginVersion = "12.4.0-pre";
|
||||
|
||||
export type BucketAggregation = (DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested);
|
||||
|
||||
|
||||
@@ -231,10 +231,6 @@ export const defaultVariableModel: Partial<VariableModel> = {
|
||||
* Option to be selected in a variable.
|
||||
*/
|
||||
export interface VariableOption {
|
||||
/**
|
||||
* Additional properties for multi-props variables
|
||||
*/
|
||||
properties?: Record<string, string>;
|
||||
/**
|
||||
* Whether the option is selected or not
|
||||
*/
|
||||
|
||||
@@ -715,9 +715,7 @@ VariableOption: {
|
||||
// Text to be displayed for the option
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
value: string | [...string]
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
-2
@@ -903,8 +903,6 @@ type VariableOption struct {
|
||||
Text StringOrArrayOfString `json:"text"`
|
||||
// Value of the option
|
||||
Value StringOrArrayOfString `json:"value"`
|
||||
// Additional properties for multi-props variables
|
||||
Properties map[string]string `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// NewVariableOption creates a new VariableOption object.
|
||||
|
||||
@@ -24,24 +24,6 @@ func TestMain(m *testing.M) {
|
||||
testsuite.Run(m)
|
||||
}
|
||||
|
||||
// mockElasticsearchHandler returns a handler that mocks Elasticsearch endpoints.
|
||||
// It responds to GET / with cluster info (required for datasource initialization)
|
||||
// and returns 401 Unauthorized for all other requests.
|
||||
func mockElasticsearchHandler(onRequest func(r *http.Request)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"version":{"build_flavor":"default","number":"8.0.0"}}`))
|
||||
default:
|
||||
if onRequest != nil {
|
||||
onRequest(r)
|
||||
}
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationElasticsearch(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
@@ -53,8 +35,9 @@ func TestIntegrationElasticsearch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
var outgoingRequest *http.Request
|
||||
outgoingServer := httptest.NewServer(mockElasticsearchHandler(func(r *http.Request) {
|
||||
outgoingServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
outgoingRequest = r
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}))
|
||||
t.Cleanup(outgoingServer.Close)
|
||||
|
||||
|
||||
@@ -639,7 +639,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "\u003e=11.6.0",
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
|
||||
@@ -3912,13 +3912,6 @@
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"properties": {
|
||||
"description": "Additional properties for multi-props variables",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"selected": {
|
||||
"description": "Whether the option is selected or not",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -3939,13 +3939,6 @@
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"properties": {
|
||||
"description": "Additional properties for multi-props variables",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"selected": {
|
||||
"description": "Whether the option is selected or not",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -92,7 +92,7 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
|
||||
}, nil
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v3/projects/%s/metricDescriptors", dsInfo.services[cloudMonitor].url, defaultProject)
|
||||
url := fmt.Sprintf("%v/v3/projects/%v/metricDescriptors", dsInfo.services[cloudMonitor].url, defaultProject)
|
||||
request, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -139,7 +139,6 @@ type datasourceInfo struct {
|
||||
defaultProject string
|
||||
clientEmail string
|
||||
tokenUri string
|
||||
universeDomain string
|
||||
services map[string]datasourceService
|
||||
privateKey string
|
||||
usingImpersonation bool
|
||||
@@ -151,7 +150,6 @@ type datasourceJSONData struct {
|
||||
DefaultProject string `json:"defaultProject"`
|
||||
ClientEmail string `json:"clientEmail"`
|
||||
TokenURI string `json:"tokenUri"`
|
||||
UniverseDomain string `json:"universeDomain"`
|
||||
UsingImpersonation bool `json:"usingImpersonation"`
|
||||
ServiceAccountToImpersonate string `json:"serviceAccountToImpersonate"`
|
||||
}
|
||||
@@ -181,7 +179,6 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
|
||||
defaultProject: jsonData.DefaultProject,
|
||||
clientEmail: jsonData.ClientEmail,
|
||||
tokenUri: jsonData.TokenURI,
|
||||
universeDomain: jsonData.UniverseDomain,
|
||||
usingImpersonation: jsonData.UsingImpersonation,
|
||||
serviceAccountToImpersonate: jsonData.ServiceAccountToImpersonate,
|
||||
services: map[string]datasourceService{},
|
||||
@@ -197,13 +194,13 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for name := range routes {
|
||||
for name, info := range routes {
|
||||
client, err := newHTTPClient(dsInfo, opts, &httpClientProvider, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dsInfo.services[name] = datasourceService{
|
||||
url: buildURL(name, dsInfo.universeDomain),
|
||||
url: info.url,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ type routeInfo struct {
|
||||
var routes = map[string]routeInfo{
|
||||
cloudMonitor: {
|
||||
method: "GET",
|
||||
url: "https://monitoring.",
|
||||
url: "https://monitoring.googleapis.com",
|
||||
scopes: []string{cloudMonitorScope},
|
||||
},
|
||||
resourceManager: {
|
||||
method: "GET",
|
||||
url: "https://cloudresourcemanager.",
|
||||
url: "https://cloudresourcemanager.googleapis.com",
|
||||
scopes: []string{resourceManagerScope},
|
||||
},
|
||||
}
|
||||
@@ -68,13 +68,6 @@ func getMiddleware(model *datasourceInfo, routePath string) (httpclient.Middlewa
|
||||
return tokenprovider.AuthMiddleware(provider), nil
|
||||
}
|
||||
|
||||
func buildURL(route string, universeDomain string) string {
|
||||
if universeDomain == "" {
|
||||
universeDomain = "googleapis.com"
|
||||
}
|
||||
return routes[route].url + universeDomain
|
||||
}
|
||||
|
||||
func newHTTPClient(model *datasourceInfo, opts httpclient.Options, clientProvider *httpclient.Provider, route string) (*http.Client, error) {
|
||||
m, err := getMiddleware(model, route)
|
||||
if err != nil {
|
||||
|
||||
@@ -111,7 +111,7 @@ func Test_setRequestVariables(t *testing.T) {
|
||||
im: &fakeInstance{
|
||||
services: map[string]datasourceService{
|
||||
cloudMonitor: {
|
||||
url: buildURL(cloudMonitor, "googleapis.com"),
|
||||
url: routes[cloudMonitor].url,
|
||||
client: &http.Client{},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,8 +3,8 @@ package elasticsearch
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
// addDateHistogramAgg adds a date histogram aggregation to the aggregation builder
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
// Used in logging to mark a stage
|
||||
@@ -34,7 +35,6 @@ type DatasourceInfo struct {
|
||||
Interval string
|
||||
MaxConcurrentShardRequests int64
|
||||
IncludeFrozen bool
|
||||
ClusterInfo ClusterInfo
|
||||
}
|
||||
|
||||
type ConfiguredFields struct {
|
||||
@@ -159,7 +159,7 @@ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearch
|
||||
resSpan.End()
|
||||
}()
|
||||
|
||||
improvedParsingEnabled := isFeatureEnabled(c.ctx, "elasticsearchImprovedParsing")
|
||||
improvedParsingEnabled := isFeatureEnabled(c.ctx, featuremgmt.FlagElasticsearchImprovedParsing)
|
||||
msr, err := c.parser.parseMultiSearchResponse(res.Body, improvedParsingEnabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -197,11 +197,7 @@ func (c *baseClientImpl) createMultiSearchRequests(searchRequests []*SearchReque
|
||||
|
||||
func (c *baseClientImpl) getMultiSearchQueryParameters() string {
|
||||
var qs []string
|
||||
// if the build flavor is not serverless, we can use the max concurrent shard requests
|
||||
// this is because serverless clusters do not support max concurrent shard requests
|
||||
if !c.ds.ClusterInfo.IsServerless() && c.ds.MaxConcurrentShardRequests > 0 {
|
||||
qs = append(qs, fmt.Sprintf("max_concurrent_shard_requests=%d", c.ds.MaxConcurrentShardRequests))
|
||||
}
|
||||
qs = append(qs, fmt.Sprintf("max_concurrent_shard_requests=%d", c.ds.MaxConcurrentShardRequests))
|
||||
|
||||
if c.ds.IncludeFrozen {
|
||||
qs = append(qs, "ignore_throttled=false")
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
func TestClient_ExecuteMultisearch(t *testing.T) {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package es
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type VersionInfo struct {
|
||||
BuildFlavor string `json:"build_flavor"`
|
||||
}
|
||||
|
||||
// ClusterInfo represents Elasticsearch cluster information returned from the root endpoint.
|
||||
// It is used to determine cluster capabilities and configuration like whether the cluster is serverless.
|
||||
type ClusterInfo struct {
|
||||
Version VersionInfo `json:"version"`
|
||||
}
|
||||
|
||||
const (
|
||||
BuildFlavorServerless = "serverless"
|
||||
)
|
||||
|
||||
// GetClusterInfo fetches cluster information from the Elasticsearch root endpoint.
|
||||
// It returns the cluster build flavor which is used to determine if the cluster is serverless.
|
||||
func GetClusterInfo(httpCli *http.Client, url string) (clusterInfo ClusterInfo, err error) {
|
||||
resp, err := httpCli.Get(url)
|
||||
if err != nil {
|
||||
return ClusterInfo{}, fmt.Errorf("error getting ES cluster info: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return ClusterInfo{}, fmt.Errorf("unexpected status code %d getting ES cluster info", resp.StatusCode)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if closeErr := resp.Body.Close(); closeErr != nil && err == nil {
|
||||
err = fmt.Errorf("error closing response body: %w", closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&clusterInfo)
|
||||
if err != nil {
|
||||
return ClusterInfo{}, fmt.Errorf("error decoding ES cluster info: %w", err)
|
||||
}
|
||||
|
||||
return clusterInfo, nil
|
||||
}
|
||||
|
||||
func (ci ClusterInfo) IsServerless() bool {
|
||||
return ci.Version.BuildFlavor == BuildFlavorServerless
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
package es
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetClusterInfo(t *testing.T) {
|
||||
t.Run("Should successfully get cluster info", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
_, err := rw.Write([]byte(`{
|
||||
"name": "test-cluster",
|
||||
"cluster_name": "elasticsearch",
|
||||
"cluster_uuid": "abc123",
|
||||
"version": {
|
||||
"number": "8.0.0",
|
||||
"build_flavor": "default",
|
||||
"build_type": "tar",
|
||||
"build_hash": "abc123",
|
||||
"build_date": "2023-01-01T00:00:00.000Z",
|
||||
"build_snapshot": false,
|
||||
"lucene_version": "9.0.0"
|
||||
}
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, clusterInfo)
|
||||
assert.Equal(t, "default", clusterInfo.Version.BuildFlavor)
|
||||
})
|
||||
|
||||
t.Run("Should successfully get serverless cluster info", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
_, err := rw.Write([]byte(`{
|
||||
"name": "serverless-cluster",
|
||||
"cluster_name": "elasticsearch",
|
||||
"cluster_uuid": "def456",
|
||||
"version": {
|
||||
"number": "8.11.0",
|
||||
"build_flavor": "serverless",
|
||||
"build_type": "docker",
|
||||
"build_hash": "def456",
|
||||
"build_date": "2023-11-01T00:00:00.000Z",
|
||||
"build_snapshot": false,
|
||||
"lucene_version": "9.8.0"
|
||||
}
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, clusterInfo)
|
||||
assert.Equal(t, "serverless", clusterInfo.Version.BuildFlavor)
|
||||
assert.True(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should return error when HTTP request fails", func(t *testing.T) {
|
||||
clusterInfo, err := GetClusterInfo(http.DefaultClient, "http://invalid-url-that-does-not-exist.local:9999")
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ClusterInfo{}, clusterInfo)
|
||||
assert.Contains(t, err.Error(), "error getting ES cluster info")
|
||||
})
|
||||
|
||||
t.Run("Should return error when response body is invalid JSON", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
_, err := rw.Write([]byte(`{"invalid json`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ClusterInfo{}, clusterInfo)
|
||||
assert.Contains(t, err.Error(), "error decoding ES cluster info")
|
||||
})
|
||||
|
||||
t.Run("Should handle empty version object", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
_, err := rw.Write([]byte(`{
|
||||
"name": "test-cluster",
|
||||
"version": {}
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ClusterInfo{}, clusterInfo)
|
||||
assert.Equal(t, "", clusterInfo.Version.BuildFlavor)
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should handle HTTP error status codes", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
_, err := rw.Write([]byte(`{"error": "Unauthorized"}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ClusterInfo{}, clusterInfo)
|
||||
assert.Contains(t, err.Error(), "unexpected status code 401 getting ES cluster info")
|
||||
})
|
||||
}
|
||||
|
||||
func TestClusterInfo_IsServerless(t *testing.T) {
|
||||
t.Run("Should return true when build_flavor is serverless", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{
|
||||
Version: VersionInfo{
|
||||
BuildFlavor: BuildFlavorServerless,
|
||||
},
|
||||
}
|
||||
|
||||
assert.True(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should return false when build_flavor is default", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{
|
||||
Version: VersionInfo{
|
||||
BuildFlavor: "default",
|
||||
},
|
||||
}
|
||||
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should return false when build_flavor is empty", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{
|
||||
Version: VersionInfo{
|
||||
BuildFlavor: "",
|
||||
},
|
||||
}
|
||||
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should return false when build_flavor is unknown value", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{
|
||||
Version: VersionInfo{
|
||||
BuildFlavor: "unknown",
|
||||
},
|
||||
}
|
||||
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("should return false when cluster info is empty", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{}
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
func TestSearchRequest(t *testing.T) {
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
// processQuery processes a single query and adds it to the multi-search request builder
|
||||
|
||||
@@ -3,7 +3,7 @@ package elasticsearch
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
// setFloatPath converts a string value at the specified path to float64
|
||||
|
||||
@@ -88,14 +88,6 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins
|
||||
httpCliOpts.SigV4.Service = "es"
|
||||
}
|
||||
|
||||
apiKeyAuth, ok := jsonData["apiKeyAuth"].(bool)
|
||||
if ok && apiKeyAuth {
|
||||
apiKey := settings.DecryptedSecureJSONData["apiKey"]
|
||||
if apiKey != "" {
|
||||
httpCliOpts.Header.Add("Authorization", "ApiKey "+apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
httpCli, err := httpClientProvider.New(httpCliOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -159,11 +151,6 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins
|
||||
includeFrozen = false
|
||||
}
|
||||
|
||||
clusterInfo, err := es.GetClusterInfo(httpCli, settings.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configuredFields := es.ConfiguredFields{
|
||||
TimeField: timeField,
|
||||
LogLevelField: logLevelField,
|
||||
@@ -179,7 +166,6 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins
|
||||
ConfiguredFields: configuredFields,
|
||||
Interval: interval,
|
||||
IncludeFrozen: includeFrozen,
|
||||
ClusterInfo: clusterInfo,
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ package elasticsearch
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
@@ -20,26 +18,8 @@ type datasourceInfo struct {
|
||||
Interval string `json:"interval"`
|
||||
}
|
||||
|
||||
// mockElasticsearchServer creates a test HTTP server that mocks Elasticsearch cluster info endpoint
|
||||
func mockElasticsearchServer() *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// Return a mock Elasticsearch cluster info response
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"version": map[string]interface{}{
|
||||
"build_flavor": "serverless",
|
||||
"number": "8.0.0",
|
||||
},
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
func TestNewInstanceSettings(t *testing.T) {
|
||||
t.Run("fields exist", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: 5,
|
||||
@@ -48,7 +28,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -58,9 +37,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
|
||||
t.Run("timeField", func(t *testing.T) {
|
||||
t.Run("is nil", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
MaxConcurrentShardRequests: 5,
|
||||
Interval: "Daily",
|
||||
@@ -70,7 +46,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -79,9 +54,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("is empty", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
MaxConcurrentShardRequests: 5,
|
||||
Interval: "Daily",
|
||||
@@ -92,7 +64,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -103,9 +74,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
|
||||
t.Run("maxConcurrentShardRequests", func(t *testing.T) {
|
||||
t.Run("no maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
}
|
||||
@@ -113,7 +81,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -123,9 +90,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("string maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: "10",
|
||||
@@ -134,7 +98,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -144,9 +107,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("number maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: 10,
|
||||
@@ -155,7 +115,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -165,9 +124,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("zero maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: 0,
|
||||
@@ -176,7 +132,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -186,9 +141,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("negative maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: -10,
|
||||
@@ -197,7 +149,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -207,9 +158,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("float maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: 10.5,
|
||||
@@ -218,7 +166,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -228,9 +175,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("invalid maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: "invalid",
|
||||
@@ -239,7 +183,6 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
|
||||
Message: "Health check failed: Failed to get data source info",
|
||||
}, nil
|
||||
}
|
||||
|
||||
healthStatusUrl, err := url.Parse(ds.URL)
|
||||
if err != nil {
|
||||
logger.Error("Failed to parse data source URL", "error", err)
|
||||
@@ -37,14 +38,6 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
|
||||
}, nil
|
||||
}
|
||||
|
||||
// If the cluster is serverless, return a healthy result
|
||||
if ds.ClusterInfo.IsServerless() {
|
||||
return &backend.CheckHealthResult{
|
||||
Status: backend.HealthStatusOk,
|
||||
Message: "Elasticsearch Serverless data source is healthy.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// check that ES is healthy
|
||||
healthStatusUrl.Path = path.Join(healthStatusUrl.Path, "_cluster/health")
|
||||
healthStatusUrl.RawQuery = "wait_for_status=yellow"
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
// metricsResponseProcessor handles processing of metrics query responses
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
// Query represents the time series query model of the datasource
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
func parseQuery(tsdbQuery []backend.DataQuery, logger log.Logger) ([]*Query, error) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
// AggregationParser parses raw Elasticsearch DSL aggregations
|
||||
|
||||
@@ -15,9 +15,9 @@ import (
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/instrumentation"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
// flatten flattens multi-level objects to single level objects. It uses dot notation to join keys.
|
||||
|
||||
@@ -1,582 +0,0 @@
|
||||
// Package simplejson provides a wrapper for arbitrary JSON objects that adds methods to access properties.
|
||||
// Use of this package in place of types and the standard library's encoding/json package is strongly discouraged.
|
||||
//
|
||||
// Don't lint for stale code, since it's a copied library and we might as well keep the whole thing.
|
||||
// nolint:unused
|
||||
package simplejson
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// returns the current implementation version
|
||||
func Version() string {
|
||||
return "0.5.0"
|
||||
}
|
||||
|
||||
type Json struct {
|
||||
data any
|
||||
}
|
||||
|
||||
func (j *Json) FromDB(data []byte) error {
|
||||
j.data = make(map[string]any)
|
||||
|
||||
dec := json.NewDecoder(bytes.NewBuffer(data))
|
||||
dec.UseNumber()
|
||||
return dec.Decode(&j.data)
|
||||
}
|
||||
|
||||
func (j *Json) ToDB() ([]byte, error) {
|
||||
if j == nil || j.data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return j.Encode()
|
||||
}
|
||||
|
||||
func (j *Json) Scan(val any) error {
|
||||
switch v := val.(type) {
|
||||
case []byte:
|
||||
if len(v) == 0 {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(v, &j)
|
||||
case string:
|
||||
if len(v) == 0 {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal([]byte(v), &j)
|
||||
default:
|
||||
return fmt.Errorf("unsupported type: %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
func (j *Json) Value() (driver.Value, error) {
|
||||
return j.ToDB()
|
||||
}
|
||||
|
||||
// DeepCopyInto creates a copy by serializing JSON
|
||||
func (j *Json) DeepCopyInto(out *Json) {
|
||||
b, err := j.Encode()
|
||||
if err == nil {
|
||||
_ = out.UnmarshalJSON(b)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy will make a deep copy of the JSON object
|
||||
func (j *Json) DeepCopy() *Json {
|
||||
if j == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Json)
|
||||
j.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// NewJson returns a pointer to a new `Json` object
|
||||
// after unmarshaling `body` bytes
|
||||
func NewJson(body []byte) (*Json, error) {
|
||||
j := new(Json)
|
||||
err := j.UnmarshalJSON(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return j, nil
|
||||
}
|
||||
|
||||
// MustJson returns a pointer to a new `Json` object, panicking if `body` cannot be parsed.
|
||||
func MustJson(body []byte) *Json {
|
||||
j, err := NewJson(body)
|
||||
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("could not unmarshal JSON: %q", err))
|
||||
}
|
||||
|
||||
return j
|
||||
}
|
||||
|
||||
// New returns a pointer to a new, empty `Json` object
|
||||
func New() *Json {
|
||||
return &Json{
|
||||
data: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
// NewFromAny returns a pointer to a new `Json` object with provided data.
|
||||
func NewFromAny(data any) *Json {
|
||||
return &Json{data: data}
|
||||
}
|
||||
|
||||
// Interface returns the underlying data
|
||||
func (j *Json) Interface() any {
|
||||
return j.data
|
||||
}
|
||||
|
||||
// Encode returns its marshaled data as `[]byte`
|
||||
func (j *Json) Encode() ([]byte, error) {
|
||||
return j.MarshalJSON()
|
||||
}
|
||||
|
||||
// EncodePretty returns its marshaled data as `[]byte` with indentation
|
||||
func (j *Json) EncodePretty() ([]byte, error) {
|
||||
return json.MarshalIndent(&j.data, "", " ")
|
||||
}
|
||||
|
||||
// Implements the json.Marshaler interface.
|
||||
func (j *Json) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&j.data)
|
||||
}
|
||||
|
||||
// Set modifies `Json` map by `key` and `value`
|
||||
// Useful for changing single key/value in a `Json` object easily.
|
||||
func (j *Json) Set(key string, val any) {
|
||||
m, err := j.Map()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
m[key] = val
|
||||
}
|
||||
|
||||
// SetPath modifies `Json`, recursively checking/creating map keys for the supplied path,
|
||||
// and then finally writing in the value
|
||||
func (j *Json) SetPath(branch []string, val any) {
|
||||
if len(branch) == 0 {
|
||||
j.data = val
|
||||
return
|
||||
}
|
||||
|
||||
// in order to insert our branch, we need map[string]any
|
||||
if _, ok := (j.data).(map[string]any); !ok {
|
||||
// have to replace with something suitable
|
||||
j.data = make(map[string]any)
|
||||
}
|
||||
curr := j.data.(map[string]any)
|
||||
|
||||
for i := 0; i < len(branch)-1; i++ {
|
||||
b := branch[i]
|
||||
// key exists?
|
||||
if _, ok := curr[b]; !ok {
|
||||
n := make(map[string]any)
|
||||
curr[b] = n
|
||||
curr = n
|
||||
continue
|
||||
}
|
||||
|
||||
// make sure the value is the right sort of thing
|
||||
if _, ok := curr[b].(map[string]any); !ok {
|
||||
// have to replace with something suitable
|
||||
n := make(map[string]any)
|
||||
curr[b] = n
|
||||
}
|
||||
|
||||
curr = curr[b].(map[string]any)
|
||||
}
|
||||
|
||||
// add remaining k/v
|
||||
curr[branch[len(branch)-1]] = val
|
||||
}
|
||||
|
||||
// Del modifies `Json` map by deleting `key` if it is present.
|
||||
func (j *Json) Del(key string) {
|
||||
m, err := j.Map()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
delete(m, key)
|
||||
}
|
||||
|
||||
// Get returns a pointer to a new `Json` object
|
||||
// for `key` in its `map` representation
|
||||
//
|
||||
// useful for chaining operations (to traverse a nested JSON):
|
||||
//
|
||||
// js.Get("top_level").Get("dict").Get("value").Int()
|
||||
func (j *Json) Get(key string) *Json {
|
||||
m, err := j.Map()
|
||||
if err == nil {
|
||||
if val, ok := m[key]; ok {
|
||||
return &Json{val}
|
||||
}
|
||||
}
|
||||
return &Json{nil}
|
||||
}
|
||||
|
||||
// GetPath searches for the item as specified by the branch
|
||||
// without the need to deep dive using Get()'s.
|
||||
//
|
||||
// js.GetPath("top_level", "dict")
|
||||
func (j *Json) GetPath(branch ...string) *Json {
|
||||
jin := j
|
||||
for _, p := range branch {
|
||||
jin = jin.Get(p)
|
||||
}
|
||||
return jin
|
||||
}
|
||||
|
||||
// GetIndex returns a pointer to a new `Json` object
|
||||
// for `index` in its `array` representation
|
||||
//
|
||||
// this is the analog to Get when accessing elements of
|
||||
// a json array instead of a json object:
|
||||
//
|
||||
// js.Get("top_level").Get("array").GetIndex(1).Get("key").Int()
|
||||
func (j *Json) GetIndex(index int) *Json {
|
||||
a, err := j.Array()
|
||||
if err == nil {
|
||||
if len(a) > index {
|
||||
return &Json{a[index]}
|
||||
}
|
||||
}
|
||||
return &Json{nil}
|
||||
}
|
||||
|
||||
// CheckGetIndex returns a pointer to a new `Json` object
|
||||
// for `index` in its `array` representation, and a `bool`
|
||||
// indicating success or failure
|
||||
//
|
||||
// useful for chained operations when success is important:
|
||||
//
|
||||
// if data, ok := js.Get("top_level").CheckGetIndex(0); ok {
|
||||
// log.Println(data)
|
||||
// }
|
||||
func (j *Json) CheckGetIndex(index int) (*Json, bool) {
|
||||
a, err := j.Array()
|
||||
if err == nil {
|
||||
if len(a) > index {
|
||||
return &Json{a[index]}, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// SetIndex modifies `Json` array by `index` and `value`
|
||||
// for `index` in its `array` representation
|
||||
func (j *Json) SetIndex(index int, val any) {
|
||||
a, err := j.Array()
|
||||
if err == nil {
|
||||
if len(a) > index {
|
||||
a[index] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckGet returns a pointer to a new `Json` object and
|
||||
// a `bool` identifying success or failure
|
||||
//
|
||||
// useful for chained operations when success is important:
|
||||
//
|
||||
// if data, ok := js.Get("top_level").CheckGet("inner"); ok {
|
||||
// log.Println(data)
|
||||
// }
|
||||
func (j *Json) CheckGet(key string) (*Json, bool) {
|
||||
m, err := j.Map()
|
||||
if err == nil {
|
||||
if val, ok := m[key]; ok {
|
||||
return &Json{val}, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Map type asserts to `map`
|
||||
func (j *Json) Map() (map[string]any, error) {
|
||||
if m, ok := (j.data).(map[string]any); ok {
|
||||
return m, nil
|
||||
}
|
||||
return nil, errors.New("type assertion to map[string]any failed")
|
||||
}
|
||||
|
||||
// Array type asserts to an `array`
|
||||
func (j *Json) Array() ([]any, error) {
|
||||
if a, ok := (j.data).([]any); ok {
|
||||
return a, nil
|
||||
}
|
||||
return nil, errors.New("type assertion to []any failed")
|
||||
}
|
||||
|
||||
// Bool type asserts to `bool`
|
||||
func (j *Json) Bool() (bool, error) {
|
||||
if s, ok := (j.data).(bool); ok {
|
||||
return s, nil
|
||||
}
|
||||
return false, errors.New("type assertion to bool failed")
|
||||
}
|
||||
|
||||
// String type asserts to `string`
|
||||
func (j *Json) String() (string, error) {
|
||||
if s, ok := (j.data).(string); ok {
|
||||
return s, nil
|
||||
}
|
||||
return "", errors.New("type assertion to string failed")
|
||||
}
|
||||
|
||||
// Bytes type asserts to `[]byte`
|
||||
func (j *Json) Bytes() ([]byte, error) {
|
||||
if s, ok := (j.data).(string); ok {
|
||||
return []byte(s), nil
|
||||
}
|
||||
return nil, errors.New("type assertion to []byte failed")
|
||||
}
|
||||
|
||||
// StringArray type asserts to an `array` of `string`
|
||||
func (j *Json) StringArray() ([]string, error) {
|
||||
arr, err := j.Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
retArr := make([]string, 0, len(arr))
|
||||
for _, a := range arr {
|
||||
if a == nil {
|
||||
retArr = append(retArr, "")
|
||||
continue
|
||||
}
|
||||
s, ok := a.(string)
|
||||
if !ok {
|
||||
return nil, err
|
||||
}
|
||||
retArr = append(retArr, s)
|
||||
}
|
||||
return retArr, nil
|
||||
}
|
||||
|
||||
// MustArray guarantees the return of a `[]any` (with optional default)
|
||||
//
|
||||
// useful when you want to iterate over array values in a succinct manner:
|
||||
//
|
||||
// for i, v := range js.Get("results").MustArray() {
|
||||
// fmt.Println(i, v)
|
||||
// }
|
||||
func (j *Json) MustArray(args ...[]any) []any {
|
||||
var def []any
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustArray() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
a, err := j.Array()
|
||||
if err == nil {
|
||||
return a
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MustMap guarantees the return of a `map[string]any` (with optional default)
|
||||
//
|
||||
// useful when you want to iterate over map values in a succinct manner:
|
||||
//
|
||||
// for k, v := range js.Get("dictionary").MustMap() {
|
||||
// fmt.Println(k, v)
|
||||
// }
|
||||
func (j *Json) MustMap(args ...map[string]any) map[string]any {
|
||||
var def map[string]any
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustMap() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
a, err := j.Map()
|
||||
if err == nil {
|
||||
return a
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MustString guarantees the return of a `string` (with optional default)
|
||||
//
|
||||
// useful when you explicitly want a `string` in a single value return context:
|
||||
//
|
||||
// myFunc(js.Get("param1").MustString(), js.Get("optional_param").MustString("my_default"))
|
||||
func (j *Json) MustString(args ...string) string {
|
||||
var def string
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustString() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
s, err := j.String()
|
||||
if err == nil {
|
||||
return s
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MustStringArray guarantees the return of a `[]string` (with optional default)
|
||||
//
|
||||
// useful when you want to iterate over array values in a succinct manner:
|
||||
//
|
||||
// for i, s := range js.Get("results").MustStringArray() {
|
||||
// fmt.Println(i, s)
|
||||
// }
|
||||
func (j *Json) MustStringArray(args ...[]string) []string {
|
||||
var def []string
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustStringArray() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
a, err := j.StringArray()
|
||||
if err == nil {
|
||||
return a
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MustInt guarantees the return of an `int` (with optional default)
|
||||
//
|
||||
// useful when you explicitly want an `int` in a single value return context:
|
||||
//
|
||||
// myFunc(js.Get("param1").MustInt(), js.Get("optional_param").MustInt(5150))
|
||||
func (j *Json) MustInt(args ...int) int {
|
||||
var def int
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustInt() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
i, err := j.Int()
|
||||
if err == nil {
|
||||
return i
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MustFloat64 guarantees the return of a `float64` (with optional default)
|
||||
//
|
||||
// useful when you explicitly want a `float64` in a single value return context:
|
||||
//
|
||||
// myFunc(js.Get("param1").MustFloat64(), js.Get("optional_param").MustFloat64(5.150))
|
||||
func (j *Json) MustFloat64(args ...float64) float64 {
|
||||
var def float64
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustFloat64() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
f, err := j.Float64()
|
||||
if err == nil {
|
||||
return f
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MustBool guarantees the return of a `bool` (with optional default)
|
||||
//
|
||||
// useful when you explicitly want a `bool` in a single value return context:
|
||||
//
|
||||
// myFunc(js.Get("param1").MustBool(), js.Get("optional_param").MustBool(true))
|
||||
func (j *Json) MustBool(args ...bool) bool {
|
||||
var def bool
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustBool() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
b, err := j.Bool()
|
||||
if err == nil {
|
||||
return b
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MustInt64 guarantees the return of an `int64` (with optional default)
|
||||
//
|
||||
// useful when you explicitly want an `int64` in a single value return context:
|
||||
//
|
||||
// myFunc(js.Get("param1").MustInt64(), js.Get("optional_param").MustInt64(5150))
|
||||
func (j *Json) MustInt64(args ...int64) int64 {
|
||||
var def int64
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustInt64() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
i, err := j.Int64()
|
||||
if err == nil {
|
||||
return i
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MustUInt64 guarantees the return of an `uint64` (with optional default)
|
||||
//
|
||||
// useful when you explicitly want an `uint64` in a single value return context:
|
||||
//
|
||||
// myFunc(js.Get("param1").MustUint64(), js.Get("optional_param").MustUint64(5150))
|
||||
func (j *Json) MustUint64(args ...uint64) uint64 {
|
||||
var def uint64
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
def = args[0]
|
||||
default:
|
||||
log.Panicf("MustUint64() received too many arguments %d", len(args))
|
||||
}
|
||||
|
||||
i, err := j.Uint64()
|
||||
if err == nil {
|
||||
return i
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// MarshalYAML implements yaml.Marshaller.
|
||||
func (j *Json) MarshalYAML() (any, error) {
|
||||
return j.data, nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaller.
|
||||
func (j *Json) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var data any
|
||||
if err := unmarshal(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
j.data = data
|
||||
return nil
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package simplejson
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Implements the json.Unmarshaler interface.
|
||||
func (j *Json) UnmarshalJSON(p []byte) error {
|
||||
dec := json.NewDecoder(bytes.NewBuffer(p))
|
||||
dec.UseNumber()
|
||||
return dec.Decode(&j.data)
|
||||
}
|
||||
|
||||
// NewFromReader returns a *Json by decoding from an io.Reader
|
||||
func NewFromReader(r io.Reader) (*Json, error) {
|
||||
j := new(Json)
|
||||
dec := json.NewDecoder(r)
|
||||
dec.UseNumber()
|
||||
err := dec.Decode(&j.data)
|
||||
return j, err
|
||||
}
|
||||
|
||||
// Float64 coerces into a float64
|
||||
func (j *Json) Float64() (float64, error) {
|
||||
switch n := j.data.(type) {
|
||||
case json.Number:
|
||||
return n.Float64()
|
||||
case float32, float64:
|
||||
return reflect.ValueOf(j.data).Float(), nil
|
||||
case int, int8, int16, int32, int64:
|
||||
return float64(reflect.ValueOf(j.data).Int()), nil
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return float64(reflect.ValueOf(j.data).Uint()), nil
|
||||
}
|
||||
return 0, errors.New("invalid value type")
|
||||
}
|
||||
|
||||
// Int coerces into an int
|
||||
func (j *Json) Int() (int, error) {
|
||||
switch n := j.data.(type) {
|
||||
case json.Number:
|
||||
i, err := n.Int64()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(i), nil
|
||||
case float32, float64:
|
||||
return int(reflect.ValueOf(j.data).Float()), nil
|
||||
case int, int8, int16, int32, int64:
|
||||
return int(reflect.ValueOf(j.data).Int()), nil
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return int(reflect.ValueOf(j.data).Uint()), nil
|
||||
}
|
||||
return 0, errors.New("invalid value type")
|
||||
}
|
||||
|
||||
// Int64 coerces into an int64
|
||||
func (j *Json) Int64() (int64, error) {
|
||||
switch n := j.data.(type) {
|
||||
case json.Number:
|
||||
return n.Int64()
|
||||
case float32, float64:
|
||||
return int64(reflect.ValueOf(j.data).Float()), nil
|
||||
case int, int8, int16, int32, int64:
|
||||
return reflect.ValueOf(j.data).Int(), nil
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return int64(reflect.ValueOf(j.data).Uint()), nil
|
||||
}
|
||||
return 0, errors.New("invalid value type")
|
||||
}
|
||||
|
||||
// Uint64 coerces into an uint64
|
||||
func (j *Json) Uint64() (uint64, error) {
|
||||
switch n := j.data.(type) {
|
||||
case json.Number:
|
||||
return strconv.ParseUint(n.String(), 10, 64)
|
||||
case float32, float64:
|
||||
return uint64(reflect.ValueOf(j.data).Float()), nil
|
||||
case int, int8, int16, int32, int64:
|
||||
return uint64(reflect.ValueOf(j.data).Int()), nil
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return reflect.ValueOf(j.data).Uint(), nil
|
||||
}
|
||||
return 0, errors.New("invalid value type")
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
package simplejson
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSimplejson(t *testing.T) {
|
||||
var ok bool
|
||||
var err error
|
||||
|
||||
js, err := NewJson([]byte(`{
|
||||
"test": {
|
||||
"string_array": ["asdf", "ghjk", "zxcv"],
|
||||
"string_array_null": ["abc", null, "efg"],
|
||||
"array": [1, "2", 3],
|
||||
"arraywithsubs": [{"subkeyone": 1},
|
||||
{"subkeytwo": 2, "subkeythree": 3}],
|
||||
"int": 10,
|
||||
"float": 5.150,
|
||||
"string": "simplejson",
|
||||
"bool": true,
|
||||
"sub_obj": {"a": 1}
|
||||
}
|
||||
}`))
|
||||
|
||||
assert.NotEqual(t, nil, js)
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
_, ok = js.CheckGet("test")
|
||||
assert.Equal(t, true, ok)
|
||||
|
||||
_, ok = js.CheckGet("missing_key")
|
||||
assert.Equal(t, false, ok)
|
||||
|
||||
aws := js.Get("test").Get("arraywithsubs")
|
||||
assert.NotEqual(t, nil, aws)
|
||||
var awsval int
|
||||
awsval, _ = aws.GetIndex(0).Get("subkeyone").Int()
|
||||
assert.Equal(t, 1, awsval)
|
||||
awsval, _ = aws.GetIndex(1).Get("subkeytwo").Int()
|
||||
assert.Equal(t, 2, awsval)
|
||||
awsval, _ = aws.GetIndex(1).Get("subkeythree").Int()
|
||||
assert.Equal(t, 3, awsval)
|
||||
|
||||
arr := js.Get("test").Get("array")
|
||||
assert.NotEqual(t, nil, arr)
|
||||
val, ok := arr.CheckGetIndex(0)
|
||||
assert.Equal(t, ok, true)
|
||||
valInt, _ := val.Int()
|
||||
assert.Equal(t, valInt, 1)
|
||||
val, ok = arr.CheckGetIndex(1)
|
||||
assert.Equal(t, ok, true)
|
||||
valStr, _ := val.String()
|
||||
assert.Equal(t, valStr, "2")
|
||||
val, ok = arr.CheckGetIndex(2)
|
||||
assert.Equal(t, ok, true)
|
||||
valInt, _ = val.Int()
|
||||
assert.Equal(t, valInt, 3)
|
||||
_, ok = arr.CheckGetIndex(3)
|
||||
assert.Equal(t, ok, false)
|
||||
|
||||
i, _ := js.Get("test").Get("int").Int()
|
||||
assert.Equal(t, 10, i)
|
||||
|
||||
f, _ := js.Get("test").Get("float").Float64()
|
||||
assert.Equal(t, 5.150, f)
|
||||
|
||||
s, _ := js.Get("test").Get("string").String()
|
||||
assert.Equal(t, "simplejson", s)
|
||||
|
||||
b, _ := js.Get("test").Get("bool").Bool()
|
||||
assert.Equal(t, true, b)
|
||||
|
||||
mi := js.Get("test").Get("int").MustInt()
|
||||
assert.Equal(t, 10, mi)
|
||||
|
||||
mi2 := js.Get("test").Get("missing_int").MustInt(5150)
|
||||
assert.Equal(t, 5150, mi2)
|
||||
|
||||
ms := js.Get("test").Get("string").MustString()
|
||||
assert.Equal(t, "simplejson", ms)
|
||||
|
||||
ms2 := js.Get("test").Get("missing_string").MustString("fyea")
|
||||
assert.Equal(t, "fyea", ms2)
|
||||
|
||||
ma2 := js.Get("test").Get("missing_array").MustArray([]any{"1", 2, "3"})
|
||||
assert.Equal(t, ma2, []any{"1", 2, "3"})
|
||||
|
||||
msa := js.Get("test").Get("string_array").MustStringArray()
|
||||
assert.Equal(t, msa[0], "asdf")
|
||||
assert.Equal(t, msa[1], "ghjk")
|
||||
assert.Equal(t, msa[2], "zxcv")
|
||||
|
||||
msa2 := js.Get("test").Get("string_array").MustStringArray([]string{"1", "2", "3"})
|
||||
assert.Equal(t, msa2[0], "asdf")
|
||||
assert.Equal(t, msa2[1], "ghjk")
|
||||
assert.Equal(t, msa2[2], "zxcv")
|
||||
|
||||
msa3 := js.Get("test").Get("missing_array").MustStringArray([]string{"1", "2", "3"})
|
||||
assert.Equal(t, msa3, []string{"1", "2", "3"})
|
||||
|
||||
mm2 := js.Get("test").Get("missing_map").MustMap(map[string]any{"found": false})
|
||||
assert.Equal(t, mm2, map[string]any{"found": false})
|
||||
|
||||
strs, err := js.Get("test").Get("string_array").StringArray()
|
||||
assert.Equal(t, err, nil)
|
||||
assert.Equal(t, strs[0], "asdf")
|
||||
assert.Equal(t, strs[1], "ghjk")
|
||||
assert.Equal(t, strs[2], "zxcv")
|
||||
|
||||
strs2, err := js.Get("test").Get("string_array_null").StringArray()
|
||||
assert.Equal(t, err, nil)
|
||||
assert.Equal(t, strs2[0], "abc")
|
||||
assert.Equal(t, strs2[1], "")
|
||||
assert.Equal(t, strs2[2], "efg")
|
||||
|
||||
gp, _ := js.GetPath("test", "string").String()
|
||||
assert.Equal(t, "simplejson", gp)
|
||||
|
||||
gp2, _ := js.GetPath("test", "int").Int()
|
||||
assert.Equal(t, 10, gp2)
|
||||
|
||||
assert.Equal(t, js.Get("test").Get("bool").MustBool(), true)
|
||||
|
||||
js.Set("float2", 300.0)
|
||||
assert.Equal(t, js.Get("float2").MustFloat64(), 300.0)
|
||||
|
||||
js.Set("test2", "setTest")
|
||||
assert.Equal(t, "setTest", js.Get("test2").MustString())
|
||||
|
||||
js.Del("test2")
|
||||
assert.NotEqual(t, "setTest", js.Get("test2").MustString())
|
||||
|
||||
js.Get("test").Get("sub_obj").Set("a", 2)
|
||||
assert.Equal(t, 2, js.Get("test").Get("sub_obj").Get("a").MustInt())
|
||||
|
||||
js.GetPath("test", "sub_obj").Set("a", 3)
|
||||
assert.Equal(t, 3, js.GetPath("test", "sub_obj", "a").MustInt())
|
||||
}
|
||||
|
||||
func TestStdlibInterfaces(t *testing.T) {
|
||||
val := new(struct {
|
||||
Name string `json:"name"`
|
||||
Params *Json `json:"params"`
|
||||
})
|
||||
val2 := new(struct {
|
||||
Name string `json:"name"`
|
||||
Params *Json `json:"params"`
|
||||
})
|
||||
|
||||
raw := `{"name":"myobject","params":{"string":"simplejson"}}`
|
||||
|
||||
assert.Equal(t, nil, json.Unmarshal([]byte(raw), val))
|
||||
|
||||
assert.Equal(t, "myobject", val.Name)
|
||||
assert.NotEqual(t, nil, val.Params.data)
|
||||
s, _ := val.Params.Get("string").String()
|
||||
assert.Equal(t, "simplejson", s)
|
||||
|
||||
p, err := json.Marshal(val)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, nil, json.Unmarshal(p, val2))
|
||||
assert.Equal(t, val, val2) // stable
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
js, err := NewJson([]byte(`{}`))
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
js.Set("baz", "bing")
|
||||
|
||||
s, err := js.GetPath("baz").String()
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "bing", s)
|
||||
}
|
||||
|
||||
func TestReplace(t *testing.T) {
|
||||
js, err := NewJson([]byte(`{}`))
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
err = js.UnmarshalJSON([]byte(`{"baz":"bing"}`))
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
s, err := js.GetPath("baz").String()
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "bing", s)
|
||||
}
|
||||
|
||||
func TestSetPath(t *testing.T) {
|
||||
js, err := NewJson([]byte(`{}`))
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
js.SetPath([]string{"foo", "bar"}, "baz")
|
||||
|
||||
s, err := js.GetPath("foo", "bar").String()
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "baz", s)
|
||||
}
|
||||
|
||||
func TestSetPathNoPath(t *testing.T) {
|
||||
js, err := NewJson([]byte(`{"some":"data","some_number":1.0,"some_bool":false}`))
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
f := js.GetPath("some_number").MustFloat64(99.0)
|
||||
assert.Equal(t, f, 1.0)
|
||||
|
||||
js.SetPath([]string{}, map[string]any{"foo": "bar"})
|
||||
|
||||
s, err := js.GetPath("foo").String()
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "bar", s)
|
||||
|
||||
f = js.GetPath("some_number").MustFloat64(99.0)
|
||||
assert.Equal(t, f, 99.0)
|
||||
}
|
||||
|
||||
func TestPathWillAugmentExisting(t *testing.T) {
|
||||
js, err := NewJson([]byte(`{"this":{"a":"aa","b":"bb","c":"cc"}}`))
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
js.SetPath([]string{"this", "d"}, "dd")
|
||||
|
||||
cases := []struct {
|
||||
path []string
|
||||
outcome string
|
||||
}{
|
||||
{
|
||||
path: []string{"this", "a"},
|
||||
outcome: "aa",
|
||||
},
|
||||
{
|
||||
path: []string{"this", "b"},
|
||||
outcome: "bb",
|
||||
},
|
||||
{
|
||||
path: []string{"this", "c"},
|
||||
outcome: "cc",
|
||||
},
|
||||
{
|
||||
path: []string{"this", "d"},
|
||||
outcome: "dd",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
s, err := js.GetPath(tc.path...).String()
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, tc.outcome, s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathWillOverwriteExisting(t *testing.T) {
|
||||
// notice how "a" is 0.1 - but then we'll try to set at path a, foo
|
||||
js, err := NewJson([]byte(`{"this":{"a":0.1,"b":"bb","c":"cc"}}`))
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
js.SetPath([]string{"this", "a", "foo"}, "bar")
|
||||
|
||||
s, err := js.GetPath("this", "a", "foo").String()
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "bar", s)
|
||||
}
|
||||
|
||||
func TestMustJson(t *testing.T) {
|
||||
js := MustJson([]byte(`{"foo": "bar"}`))
|
||||
assert.Equal(t, js.Get("foo").MustString(), "bar")
|
||||
|
||||
assert.PanicsWithValue(t, "could not unmarshal JSON: \"unexpected EOF\"", func() {
|
||||
MustJson([]byte(`{`))
|
||||
})
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
elasticsearch "github.com/grafana/grafana/pkg/tsdb/elasticsearch"
|
||||
)
|
||||
|
||||
var (
|
||||
_ backend.QueryDataHandler = (*Datasource)(nil)
|
||||
_ backend.CheckHealthHandler = (*Datasource)(nil)
|
||||
_ backend.CallResourceHandler = (*Datasource)(nil)
|
||||
)
|
||||
|
||||
func NewDatasource(context.Context, backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return &Datasource{
|
||||
Service: elasticsearch.ProvideService(httpclient.NewProvider()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Datasource struct {
|
||||
Service *elasticsearch.Service
|
||||
}
|
||||
|
||||
func contextualMiddlewares(ctx context.Context) context.Context {
|
||||
cfg := backend.GrafanaConfigFromContext(ctx)
|
||||
responseLimitMiddleware := httpclient.ResponseLimitMiddleware(cfg.ResponseLimit())
|
||||
ctx = httpclient.WithContextualMiddleware(ctx, responseLimitMiddleware)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
ctx = contextualMiddlewares(ctx)
|
||||
return d.Service.QueryData(ctx, req)
|
||||
}
|
||||
|
||||
func (d *Datasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
ctx = contextualMiddlewares(ctx)
|
||||
return d.Service.CallResource(ctx, req, sender)
|
||||
}
|
||||
|
||||
func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
ctx = contextualMiddlewares(ctx)
|
||||
return d.Service.CheckHealth(ctx, req)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Start listening to requests sent from Grafana. This call is blocking so
|
||||
// it won't finish until Grafana shuts down the process or the plugin choose
|
||||
// to exit by itself using os.Exit. Manage automatically manages life cycle
|
||||
// of datasource instances. It accepts datasource instance factory as first
|
||||
// argument. This factory will be automatically called on incoming request
|
||||
// from Grafana to create different instances of SampleDatasource (per datasource
|
||||
// ID). When datasource configuration changed Dispose method will be called and
|
||||
// new datasource instance created using NewSampleDatasource factory.
|
||||
if err := datasource.Manage("elasticsearch", NewDatasource, datasource.ManageOpts{}); err != nil {
|
||||
log.DefaultLogger.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DataQuery } from '@grafana/data';
|
||||
import { createMonitoringLogger, MonitoringLogger } from '@grafana/runtime';
|
||||
import store from 'app/core/store';
|
||||
import { RichHistoryQuery } from 'app/types/explore';
|
||||
|
||||
@@ -26,8 +27,15 @@ jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getBackendSrv: () => backendSrv,
|
||||
getDataSourceSrv: () => dsMock,
|
||||
createMonitoringLogger: jest.fn().mockReturnValue({ logWarning: jest.fn() }),
|
||||
}));
|
||||
|
||||
// logger is created at import so we cannot initialize inside the test
|
||||
const loggerIndex = (createMonitoringLogger as jest.Mock).mock.calls.findIndex(
|
||||
(args) => args[0] === 'features.query-history.local-storage'
|
||||
);
|
||||
const loggerMock: MonitoringLogger = (createMonitoringLogger as jest.Mock).mock.results[loggerIndex]?.value;
|
||||
|
||||
interface MockQuery extends DataQuery {
|
||||
query: string;
|
||||
}
|
||||
@@ -75,6 +83,8 @@ describe('RichHistoryLocalStorage', () => {
|
||||
jest.setSystemTime(now);
|
||||
storage = new RichHistoryLocalStorage();
|
||||
await storage.deleteAll();
|
||||
|
||||
(loggerMock.logWarning as jest.Mock).mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -223,6 +233,90 @@ describe('RichHistoryLocalStorage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('quota errors and retries', () => {
|
||||
it('should rotate and retry saving when QuotaExceededError occurs once', async () => {
|
||||
const initial = [
|
||||
{ ts: Date.now(), starred: true, comment: 'starred1', queries: [], datasourceName: 'name-of-dev-test' },
|
||||
{ ts: Date.now(), starred: false, comment: 'notStarred1', queries: [], datasourceName: 'name-of-dev-test' },
|
||||
{ ts: Date.now(), starred: true, comment: 'starred2', queries: [], datasourceName: 'name-of-dev-test' },
|
||||
];
|
||||
store.setObject(key, initial);
|
||||
|
||||
// Spy on setObject to throw once with QuotaExceededError, then call through
|
||||
const originalSetObject = store.setObject.bind(store);
|
||||
jest
|
||||
.spyOn(store, 'setObject')
|
||||
// first attempt throws and errors
|
||||
.mockImplementationOnce(() => {
|
||||
const err = new Error('quota hit');
|
||||
err.name = 'QuotaExceededError';
|
||||
throw err;
|
||||
})
|
||||
// second attempt calls through
|
||||
.mockImplementation((k: string, value: unknown) => {
|
||||
return originalSetObject(k, value);
|
||||
});
|
||||
|
||||
const result = await storage.addToRichHistory({
|
||||
starred: false,
|
||||
datasourceUid: 'dev-test',
|
||||
datasourceName: 'name-of-dev-test',
|
||||
comment: 'new',
|
||||
queries: [{ refId: 'A' }],
|
||||
});
|
||||
expect(result.richHistoryQuery).toBeDefined();
|
||||
|
||||
// After one failure, rotation removes one unstarred entry
|
||||
const saved = store.getObject<RichHistoryQuery[]>(key)!;
|
||||
expect(saved).toHaveLength(3);
|
||||
expect(saved).toMatchObject([
|
||||
expect.objectContaining({ comment: 'new' }),
|
||||
expect.objectContaining({ comment: 'starred1' }),
|
||||
expect.objectContaining({ comment: 'starred2' }),
|
||||
]);
|
||||
|
||||
// Ensure logger was called for the failure, with expected flags
|
||||
expect(loggerMock.logWarning).toHaveBeenCalled();
|
||||
const [message, payload] = (loggerMock.logWarning as jest.Mock).mock.calls[0];
|
||||
expect(message).toContain('Failed to save rich history to local storage');
|
||||
expect(payload.saveRetriesLeft).toBe('3');
|
||||
expect(payload.quotaExceededError).toBe('true');
|
||||
});
|
||||
|
||||
it('should throw StorageFull when QuotaExceededError persists for all retries and track attempts', async () => {
|
||||
store.setObject(key, [
|
||||
{ ts: Date.now(), starred: false, comment: 'notStarred1', queries: [], datasourceName: 'name-of-dev-test' },
|
||||
]);
|
||||
|
||||
const setSpy = jest.spyOn(store, 'setObject').mockImplementation(() => {
|
||||
const err = new Error('quota still hit');
|
||||
err.name = 'QuotaExceededError';
|
||||
throw err;
|
||||
});
|
||||
|
||||
await expect(
|
||||
storage.addToRichHistory({
|
||||
starred: false,
|
||||
datasourceUid: 'dev-test',
|
||||
datasourceName: 'name-of-dev-test',
|
||||
comment: 'new',
|
||||
queries: [{ refId: 'B' }],
|
||||
})
|
||||
).rejects.toMatchObject({ name: 'StorageFull' });
|
||||
|
||||
// 4 failed tracking attempts (1 save + 3 retries) should be logged (for each failed try)
|
||||
expect(loggerMock.logWarning).toHaveBeenCalledTimes(4);
|
||||
const calls = (loggerMock.logWarning as jest.Mock).mock.calls;
|
||||
expect(calls[0][0]).toContain('Failed to save rich history to local storage');
|
||||
expect(calls[0][1].saveRetriesLeft).toBe('3');
|
||||
expect(calls[1][1].saveRetriesLeft).toBe('2');
|
||||
expect(calls[2][1].saveRetriesLeft).toBe('1');
|
||||
expect(calls[3][1].saveRetriesLeft).toBe('0');
|
||||
|
||||
setSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('migration', () => {
|
||||
afterEach(() => {
|
||||
storage.deleteAll();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { find, isEqual, omit } from 'lodash';
|
||||
|
||||
import { DataQuery, SelectableValue } from '@grafana/data';
|
||||
import { RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistoryTypes';
|
||||
import { createMonitoringLogger } from '@grafana/runtime';
|
||||
import { RichHistorySearchFilters, RichHistorySettings, SortOrder } from 'app/core/utils/richHistoryTypes';
|
||||
import { RichHistoryQuery } from 'app/types/explore';
|
||||
|
||||
import store from '../store';
|
||||
@@ -26,10 +27,18 @@ export type RichHistoryLocalStorageDTO = {
|
||||
queries: DataQuery[];
|
||||
};
|
||||
|
||||
const logger = createMonitoringLogger('features.query-history.local-storage');
|
||||
|
||||
/**
|
||||
* Local storage implementation for Rich History. It keeps all entries in browser's local storage.
|
||||
*/
|
||||
export default class RichHistoryLocalStorage implements RichHistoryStorage {
|
||||
public static getLocalStorageUsageInBytes(): number {
|
||||
const richHistory: RichHistoryLocalStorageDTO[] = store.get(RICH_HISTORY_KEY) || '';
|
||||
// each character is 2 bytes
|
||||
return richHistory.length * 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return history entries based on provided filters, perform migration and clean up entries not matching retention policy.
|
||||
*/
|
||||
@@ -77,21 +86,43 @@ export default class RichHistoryLocalStorage implements RichHistoryStorage {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { queriesToKeep, limitExceeded } = checkLimits(currentRichHistoryDTOs);
|
||||
let { queriesToKeep, limitExceeded } = cleanUpUnstarredQuery(currentRichHistoryDTOs, MAX_HISTORY_ITEMS);
|
||||
|
||||
const updatedHistory: RichHistoryLocalStorageDTO[] = [newRichHistoryQueryDTO, ...queriesToKeep];
|
||||
let updatedHistory: RichHistoryLocalStorageDTO[] = [newRichHistoryQueryDTO, ...queriesToKeep];
|
||||
|
||||
try {
|
||||
store.setObject(RICH_HISTORY_KEY, updatedHistory);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'QuotaExceededError') {
|
||||
throwError(RichHistoryServiceError.StorageFull, `Saving rich history failed: ${error.message}`);
|
||||
} else {
|
||||
throw error;
|
||||
let saveRetriesLeft = 3;
|
||||
let saved = false;
|
||||
|
||||
while (!saved && saveRetriesLeft >= 0) {
|
||||
try {
|
||||
store.setObject(RICH_HISTORY_KEY, updatedHistory);
|
||||
saved = true;
|
||||
} catch (error) {
|
||||
await this.trackLocalStorageUsage('Failed to save rich history to local storage', {
|
||||
saveRetriesLeft: saveRetriesLeft.toString(),
|
||||
quotaExceededError: error instanceof Error && error.name === 'QuotaExceededError' ? 'true' : 'false',
|
||||
errorMessage: error instanceof Error ? error?.message : 'unknown',
|
||||
});
|
||||
|
||||
if (saveRetriesLeft >= 1) {
|
||||
saveRetriesLeft--;
|
||||
const { queriesToKeep: newQueriesToKeep } = cleanUpUnstarredQuery(queriesToKeep, queriesToKeep.length - 1);
|
||||
updatedHistory = [newRichHistoryQueryDTO, ...newQueriesToKeep];
|
||||
queriesToKeep = newQueriesToKeep;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.name === 'QuotaExceededError') {
|
||||
throwError(RichHistoryServiceError.StorageFull, `Saving rich history failed: ${error.message}`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (limitExceeded) {
|
||||
await this.trackLocalStorageUsage('Rich history query limit exceeded.');
|
||||
|
||||
return {
|
||||
warning: {
|
||||
type: RichHistoryStorageWarning.LimitExceeded,
|
||||
@@ -148,6 +179,33 @@ export default class RichHistoryLocalStorage implements RichHistoryStorage {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async trackLocalStorageUsage(message: string, additionalInfo?: Record<string, string>) {
|
||||
const allQueriesCount =
|
||||
(
|
||||
await this.getRichHistory({
|
||||
search: '',
|
||||
sortOrder: SortOrder.Ascending,
|
||||
datasourceFilters: [],
|
||||
starred: false,
|
||||
})
|
||||
).total || -1;
|
||||
|
||||
const allQueriesSizeInBytes = RichHistoryLocalStorage.getLocalStorageUsageInBytes();
|
||||
|
||||
const totalLocalStorageSize = calculateTotalLocalStorageSize();
|
||||
|
||||
const localStats = {
|
||||
totalLocalStorageSize: totalLocalStorageSize?.toString(),
|
||||
allQueriesSizeInBytes: allQueriesSizeInBytes?.toString(),
|
||||
allQueriesCount: allQueriesCount?.toString(),
|
||||
};
|
||||
|
||||
logger.logWarning(message, {
|
||||
...localStats,
|
||||
...additionalInfo,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateRichHistory(
|
||||
@@ -185,17 +243,20 @@ function cleanUp(richHistory: RichHistoryLocalStorageDTO[]): RichHistoryLocalSto
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the entry can be added. Throws an error if current limit has been hit.
|
||||
* Ensures the entry can be added.
|
||||
* Returns queries that should be saved back giving space for one extra query.
|
||||
*/
|
||||
export function checkLimits(queriesToKeep: RichHistoryLocalStorageDTO[]): {
|
||||
export function cleanUpUnstarredQuery(
|
||||
queriesToKeep: RichHistoryLocalStorageDTO[],
|
||||
max: number
|
||||
): {
|
||||
queriesToKeep: RichHistoryLocalStorageDTO[];
|
||||
limitExceeded: boolean;
|
||||
} {
|
||||
// remove oldest non-starred items to give space for the recent query
|
||||
let limitExceeded = false;
|
||||
let current = queriesToKeep.length - 1;
|
||||
while (current >= 0 && queriesToKeep.length >= MAX_HISTORY_ITEMS) {
|
||||
while (current >= 0 && queriesToKeep.length >= max) {
|
||||
if (!queriesToKeep[current].starred) {
|
||||
queriesToKeep.splice(current, 1);
|
||||
limitExceeded = true;
|
||||
@@ -247,3 +308,26 @@ function throwError(name: string, message: string) {
|
||||
error.name = name;
|
||||
throw error;
|
||||
}
|
||||
|
||||
function calculateTotalLocalStorageSize() {
|
||||
try {
|
||||
let total = 0;
|
||||
|
||||
// eslint-disable-next-line
|
||||
const ls = window.localStorage;
|
||||
|
||||
for (let i = 0; i < ls.length; i++) {
|
||||
const key = ls.key(i);
|
||||
if (key) {
|
||||
const value = ls.getItem(key);
|
||||
if (value) {
|
||||
total += key.length + value.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
// each character is 2 bytes
|
||||
return total * 2;
|
||||
} catch (e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export type RichHistorySearchFilters = {
|
||||
// so the resulting timerange from this will be [now - from, now - to].
|
||||
from?: number;
|
||||
to?: number;
|
||||
// true if only starred entries should be returned, false if ALL entries should be returned,
|
||||
starred: boolean;
|
||||
page?: number;
|
||||
};
|
||||
|
||||
@@ -284,7 +284,6 @@ function variableValueOptionsToVariableOptions(varState: MultiValueVariable['sta
|
||||
value: String(o.value),
|
||||
text: o.label,
|
||||
selected: Array.isArray(varState.value) ? varState.value.includes(o.value) : varState.value === o.value,
|
||||
...(o.properties && { properties: o.properties }),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
|
||||
|
||||
const isHasVariableOptions = hasVariableOptions(variable);
|
||||
const optionsForSelect = isHasVariableOptions ? variable.getOptionsForSelect(false) : [];
|
||||
const hasMultiProps = 'valuesFormat' in variable.state && variable.state.valuesFormat === 'json';
|
||||
|
||||
const onDeleteVariable = (hideModal: () => void) => () => {
|
||||
reportInteraction('Delete variable');
|
||||
@@ -124,7 +125,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
|
||||
|
||||
{EditorToRender && <EditorToRender variable={variable} onRunQuery={onRunQuery} />}
|
||||
|
||||
{isHasVariableOptions && <VariableValuesPreview options={optionsForSelect} />}
|
||||
{isHasVariableOptions && <VariableValuesPreview options={optionsForSelect} hasMultiProps={hasMultiProps} />}
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<Stack gap={2}>
|
||||
|
||||
+5
-8
@@ -10,16 +10,13 @@ import { Button, InlineFieldRow, InlineLabel, InteractiveTable, Text, useStyles2
|
||||
|
||||
export interface Props {
|
||||
options: VariableValueOption[];
|
||||
hasMultiProps?: boolean;
|
||||
}
|
||||
|
||||
const hasMultiProps = (options: Props['options']) => {
|
||||
return Object.keys(options[1]?.properties ?? options[0]?.properties ?? {}).length > 0;
|
||||
};
|
||||
|
||||
export const VariableValuesPreview = ({ options }: Props) => {
|
||||
export const VariableValuesPreview = ({ options, hasMultiProps }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const hasOptions = options.length > 0;
|
||||
const displayMultiPropsPreview = config.featureToggles.multiPropsVariables && hasOptions && hasMultiProps(options);
|
||||
const displayMultiPropsPreview = config.featureToggles.multiPropsVariables && hasMultiProps;
|
||||
|
||||
return (
|
||||
<div className={styles.previewContainer} style={{ gap: '8px' }}>
|
||||
@@ -46,8 +43,7 @@ function VariableValuesWithPropsPreview({ options }: { options: VariableValueOpt
|
||||
|
||||
return {
|
||||
data,
|
||||
// the option at index 0 can be "All" so we try to grab the column names from the 2nd option
|
||||
columns: Object.keys(data[1] ?? data[0] ?? {}).map((id) => ({
|
||||
columns: Object.keys(data[0] ?? {}).map((id) => ({
|
||||
id,
|
||||
// see https://github.com/TanStack/table/issues/1671
|
||||
header: unsanitizeKey(id),
|
||||
@@ -66,6 +62,7 @@ function VariableValuesWithPropsPreview({ options }: { options: VariableValueOpt
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const sanitizeKey = (key: string) => key.replace(/\./g, '__dot__');
|
||||
const unsanitizeKey = (key: string) => key.replace(/__dot__/g, '.');
|
||||
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ function ModalEditorMultiProps(props: ModalEditorProps) {
|
||||
{queryValidationError && <FieldValidationMessage>{queryValidationError.message}</FieldValidationMessage>}
|
||||
</div>
|
||||
<div>
|
||||
<VariableValuesPreview options={options} />
|
||||
<VariableValuesPreview options={options} hasMultiProps={valuesFormat === 'json'} />
|
||||
</div>
|
||||
</Stack>
|
||||
<Modal.ButtonRow>
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('buildCategories', () => {
|
||||
it('should add enterprise phantom plugins', () => {
|
||||
const enterprisePluginsCategory = categories[3];
|
||||
expect(enterprisePluginsCategory.title).toBe('Enterprise plugins');
|
||||
expect(enterprisePluginsCategory.plugins.length).toBe(32);
|
||||
expect(enterprisePluginsCategory.plugins.length).toBe(31);
|
||||
expect(enterprisePluginsCategory.plugins[0].name).toBe('Adobe Analytics');
|
||||
expect(enterprisePluginsCategory.plugins[enterprisePluginsCategory.plugins.length - 1].name).toBe('Zendesk');
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@ import catchpointSvg from 'img/plugins/catchpoint.svg';
|
||||
import cloudflareJpg from 'img/plugins/cloudflare.jpg';
|
||||
import cockroachdbJpg from 'img/plugins/cockroachdb.jpg';
|
||||
import datadogPng from 'img/plugins/datadog.png';
|
||||
import db2Svg from 'img/plugins/db2.svg';
|
||||
import droneSvg from 'img/plugins/drone.svg';
|
||||
import dynatracePng from 'img/plugins/dynatrace.png';
|
||||
import gitlabSvg from 'img/plugins/gitlab.svg';
|
||||
@@ -419,12 +418,6 @@ function getEnterprisePhantomPlugins(): DataSourcePluginMeta[] {
|
||||
name: 'SolarWinds',
|
||||
imgUrl: solarWindsSvg,
|
||||
}),
|
||||
getPhantomPlugin({
|
||||
id: 'grafana-ibmdb2-datasource',
|
||||
description: t('datasources.get-enterprise-phantom-plugins.description.ibmdb2-datasource', 'IBM Db2 data source'),
|
||||
name: 'IBM Db2',
|
||||
imgUrl: db2Svg,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ describe('Explore: Query History', () => {
|
||||
await waitForExplore();
|
||||
await openQueryHistory();
|
||||
|
||||
jest.spyOn(localStorage, 'checkLimits').mockImplementationOnce((queries) => {
|
||||
jest.spyOn(localStorage, 'cleanUpUnstarredQuery').mockImplementationOnce((queries) => {
|
||||
return { queriesToKeep: queries, limitExceeded: true };
|
||||
});
|
||||
|
||||
|
||||
@@ -81,17 +81,16 @@ const buildLabelPath = (label: string) => {
|
||||
};
|
||||
|
||||
const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
|
||||
if (!('options' in variable) || !variable.options[0].properties) {
|
||||
if (!('valuesFormat' in variable) || variable.valuesFormat !== 'json') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function collectFieldPaths(properties: Record<string, any>, currentPath: string) {
|
||||
function collectFieldPaths(option: Record<string, string>, currentPath: string) {
|
||||
let paths: string[] = [];
|
||||
for (const field in properties) {
|
||||
if (properties.hasOwnProperty(field)) {
|
||||
for (const field in option) {
|
||||
if (option.hasOwnProperty(field)) {
|
||||
const newPath = `${currentPath}.${field}`;
|
||||
const value = properties[field];
|
||||
const value = option[field];
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
paths = [...paths, ...collectFieldPaths(value, newPath)];
|
||||
}
|
||||
@@ -101,7 +100,11 @@ const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
|
||||
return paths;
|
||||
}
|
||||
|
||||
return collectFieldPaths(variable.options[0].properties, variable.name);
|
||||
try {
|
||||
return collectFieldPaths(JSON.parse(variable.query)[0], variable.name);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [
|
||||
|
||||
@@ -503,16 +503,13 @@ describe('linkSrv', () => {
|
||||
});
|
||||
|
||||
describe('getPanelLinksVariableSuggestions', () => {
|
||||
it('then it should return template variables, options properties and built-ins', () => {
|
||||
it('then it should return template variables, json properties and built-ins', () => {
|
||||
const templateSrvWithJsonValues = initTemplateSrv('key', [
|
||||
{
|
||||
type: 'custom',
|
||||
name: 'customServers',
|
||||
valuesFormat: 'json',
|
||||
options: [
|
||||
{ text: 'web', value: 'web', properties: { name: 'web', ip: '192.168.0.100' } },
|
||||
{ text: 'ads', value: 'ads', properties: { name: 'ads', ip: '192.168.0.142' } },
|
||||
],
|
||||
query: '[{"name":"web","ip":"192.168.0.100"},{"name":"ads","ip":"192.168.0.142"}]',
|
||||
},
|
||||
]);
|
||||
setTemplateSrv(templateSrvWithJsonValues);
|
||||
|
||||
@@ -4,6 +4,8 @@ const cloudwatchPlugin = async () =>
|
||||
await import(/* webpackChunkName: "cloudwatchPlugin" */ 'app/plugins/datasource/cloudwatch/module');
|
||||
const dashboardDSPlugin = async () =>
|
||||
await import(/* webpackChunkName "dashboardDSPlugin" */ 'app/plugins/datasource/dashboard/module');
|
||||
const elasticsearchPlugin = async () =>
|
||||
await import(/* webpackChunkName: "elasticsearchPlugin" */ 'app/plugins/datasource/elasticsearch/module');
|
||||
const grafanaPlugin = async () =>
|
||||
await import(/* webpackChunkName: "grafanaPlugin" */ 'app/plugins/datasource/grafana/module');
|
||||
const influxdbPlugin = async () =>
|
||||
@@ -73,6 +75,7 @@ const builtInPlugins: Record<string, System.Module | (() => Promise<System.Modul
|
||||
// datasources
|
||||
'core:plugin/cloudwatch': cloudwatchPlugin,
|
||||
'core:plugin/dashboard': dashboardDSPlugin,
|
||||
'core:plugin/elasticsearch': elasticsearchPlugin,
|
||||
'core:plugin/grafana': grafanaPlugin,
|
||||
'core:plugin/influxdb': influxdbPlugin,
|
||||
'core:plugin/mixed': mixedPlugin,
|
||||
|
||||
+6
-25
@@ -1,10 +1,10 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data';
|
||||
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
||||
import { ConnectionConfig } from '@grafana/google-sdk';
|
||||
import { ConfigSection, DataSourceDescription } from '@grafana/plugin-ui';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { Divider, Field, Input, SecureSocksProxySettings, Stack } from '@grafana/ui';
|
||||
import { reportInteraction, config } from '@grafana/runtime';
|
||||
import { Divider, SecureSocksProxySettings } from '@grafana/ui';
|
||||
|
||||
import { CloudMonitoringOptions, CloudMonitoringSecureJsonData } from '../../types/types';
|
||||
|
||||
@@ -36,33 +36,14 @@ export const ConfigEditor = memo(({ options, onOptionsChange }: Props) => {
|
||||
<Divider />
|
||||
<ConfigSection
|
||||
title="Additional settings"
|
||||
description="Additional settings are optional settings that can be configured for more control over your data source. This includes Secure Socks Proxy and Universe Domain."
|
||||
description="Additional settings are optional settings that can be configured for more control over your data source. This includes Secure Socks Proxy."
|
||||
isCollapsible
|
||||
isInitiallyOpen={
|
||||
options.jsonData.enableSecureSocksProxy !== undefined || options.jsonData.universeDomain !== undefined
|
||||
}
|
||||
isInitiallyOpen={options.jsonData.enableSecureSocksProxy !== undefined}
|
||||
>
|
||||
<Stack direction={'column'}>
|
||||
<Field noMargin label="Universe Domain">
|
||||
<Input
|
||||
width={50}
|
||||
value={options.jsonData.universeDomain}
|
||||
onChange={(event) =>
|
||||
updateDatasourcePluginJsonDataOption(
|
||||
{ options, onOptionsChange },
|
||||
'universeDomain',
|
||||
event.currentTarget.value
|
||||
)
|
||||
}
|
||||
placeholder="googleapis.com"
|
||||
></Input>
|
||||
</Field>
|
||||
<SecureSocksProxySettings options={options} onOptionsChange={onOptionsChange} />
|
||||
</Stack>
|
||||
<SecureSocksProxySettings options={options} onOptionsChange={onOptionsChange} />
|
||||
</ConfigSection>
|
||||
</>
|
||||
)}
|
||||
<Divider />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -38,7 +38,6 @@ export interface Aggregation {
|
||||
export interface CloudMonitoringOptions extends DataSourceOptions {
|
||||
gceDefaultProject?: string;
|
||||
enableSecureSocksProxy?: boolean;
|
||||
universeDomain?: string;
|
||||
}
|
||||
|
||||
export interface CloudMonitoringSecureJsonData extends DataSourceSecureJsonData {}
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { select } from 'react-select-event';
|
||||
|
||||
import { DateHistogram } from '../../../../dataquery.gen';
|
||||
import { DateHistogram } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
||||
|
||||
import { DateHistogramSettingsEditor } from './DateHistogramSettingsEditor';
|
||||
|
||||
+3
-3
@@ -4,9 +4,9 @@ import { GroupBase, OptionsOrGroups } from 'react-select';
|
||||
|
||||
import { InternalTimeZones, SelectableValue } from '@grafana/data';
|
||||
import { InlineField, Input, Select, TimeZonePicker } from '@grafana/ui';
|
||||
import { DateHistogram } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { calendarIntervals } from '../../../../QueryBuilder';
|
||||
import { DateHistogram } from '../../../../dataquery.gen';
|
||||
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
||||
import { useCreatableSelectPersistedBehaviour } from '../../../hooks/useCreatableSelectPersistedBehaviour';
|
||||
import { changeBucketAggregationSetting } from '../state/actions';
|
||||
@@ -37,11 +37,11 @@ const hasValue =
|
||||
const isValidNewOption = (
|
||||
inputValue: string,
|
||||
_: SelectableValue<string> | null,
|
||||
options: OptionsOrGroups<SelectableValue<string>, GroupBase<SelectableValue<string>>>
|
||||
options: OptionsOrGroups<unknown, GroupBase<unknown>>
|
||||
) => {
|
||||
// TODO: would be extremely nice here to allow only template variables and values that are
|
||||
// valid date histogram's Interval options
|
||||
const valueExists = options.some(hasValue(inputValue));
|
||||
const valueExists = (options as Array<SelectableValue<string>>).some(hasValue(inputValue));
|
||||
// we also don't want users to create "empty" values
|
||||
return !valueExists && inputValue.trim().length > 0;
|
||||
};
|
||||
|
||||
+1
-1
@@ -3,8 +3,8 @@ import { uniqueId } from 'lodash';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { InlineField, Input, QueryField } from '@grafana/ui';
|
||||
import { Filters } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { Filters } from '../../../../../dataquery.gen';
|
||||
import { useDispatch, useStatelessReducer } from '../../../../../hooks/useStatelessReducer';
|
||||
import { AddRemove } from '../../../../AddRemove';
|
||||
import { changeBucketAggregationSetting } from '../../state/actions';
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { Filter } from '../../../../../../dataquery.gen';
|
||||
import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
export const addFilter = createAction('@bucketAggregations/filter/add');
|
||||
export const removeFilter = createAction<number>('@bucketAggregations/filter/remove');
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
import { Filter } from '../../../../../../dataquery.gen';
|
||||
import { reducerTester } from '../../../../../reducerTester';
|
||||
import { reducerTester } from 'test/core/redux/reducerTester';
|
||||
|
||||
import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { addFilter, changeFilter, removeFilter } from './actions';
|
||||
import { reducer } from './reducer';
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { Action } from 'redux';
|
||||
|
||||
import { Filter } from '../../../../../../dataquery.gen';
|
||||
import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { defaultFilter } from '../utils';
|
||||
|
||||
import { addFilter, changeFilter, removeFilter } from './actions';
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
import { Filter } from '../../../../../dataquery.gen';
|
||||
import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
export const defaultFilter = (): Filter => ({ label: '', query: '*' });
|
||||
|
||||
+8
-1
@@ -1,7 +1,14 @@
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import selectEvent from 'react-select-event';
|
||||
|
||||
import { Average, Derivative, ElasticsearchDataQuery, Terms, TopMetrics } from '../../../../dataquery.gen';
|
||||
import {
|
||||
Average,
|
||||
Derivative,
|
||||
ElasticsearchDataQuery,
|
||||
Terms,
|
||||
TopMetrics,
|
||||
} from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
||||
import { renderWithESProvider } from '../../../../test-helpers/render';
|
||||
import { describeMetric } from '../../../../utils';
|
||||
|
||||
+9
-3
@@ -2,9 +2,15 @@ import { uniqueId } from 'lodash';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { InlineField, Input, Select } from '@grafana/ui';
|
||||
import { InlineField, Select, Input } from '@grafana/ui';
|
||||
import {
|
||||
Terms,
|
||||
ExtendedStats,
|
||||
ExtendedStatMetaType,
|
||||
Percentiles,
|
||||
MetricAggregation,
|
||||
} from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { ExtendedStats, MetricAggregation, Percentiles, Terms } from '../../../../dataquery.gen';
|
||||
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
||||
import { describeMetric } from '../../../../utils';
|
||||
import { useQuery } from '../../ElasticsearchQueryContext';
|
||||
@@ -99,7 +105,7 @@ function createOrderByOptionsForExtendedStats(metric: ExtendedStats): Selectable
|
||||
if (!metric.meta) {
|
||||
return [];
|
||||
}
|
||||
const metaKeys = Object.keys(metric.meta);
|
||||
const metaKeys = Object.keys(metric.meta) as ExtendedStatMetaType[];
|
||||
return metaKeys
|
||||
.filter((key) => metric.meta?.[key])
|
||||
.map((key) => {
|
||||
|
||||
+1
-1
@@ -2,8 +2,8 @@ import { uniqueId } from 'lodash';
|
||||
import { ComponentProps, useRef } from 'react';
|
||||
|
||||
import { InlineField, Input } from '@grafana/ui';
|
||||
import { BucketAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { BucketAggregation } from '../../../../dataquery.gen';
|
||||
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
||||
import { SettingsEditorContainer } from '../../SettingsEditorContainer';
|
||||
import { changeBucketAggregationSetting } from '../state/actions';
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
import { BucketAggregation } from '../../../../dataquery.gen';
|
||||
import { BucketAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { defaultGeoHashPrecisionString } from '../../../../queryDef';
|
||||
import { convertOrderByToMetricId, describeMetric } from '../../../../utils';
|
||||
import { describeMetric, convertOrderByToMetricId } from '../../../../utils';
|
||||
import { useQuery } from '../../ElasticsearchQueryContext';
|
||||
import { bucketAggregationConfig, orderByOptions, orderOptions } from '../utils';
|
||||
|
||||
|
||||
+5
-1
@@ -1,6 +1,10 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { BucketAggregation, BucketAggregationType, BucketAggregationWithField } from '../../../../dataquery.gen';
|
||||
import {
|
||||
BucketAggregation,
|
||||
BucketAggregationType,
|
||||
BucketAggregationWithField,
|
||||
} from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
export const addBucketAggregation = createAction<BucketAggregation['id']>('@bucketAggs/add');
|
||||
export const removeBucketAggregation = createAction<BucketAggregation['id']>('@bucketAggs/remove');
|
||||
|
||||
+6
-1
@@ -1,4 +1,9 @@
|
||||
import { BucketAggregation, DateHistogram, ElasticsearchDataQuery } from '../../../../dataquery.gen';
|
||||
import {
|
||||
BucketAggregation,
|
||||
DateHistogram,
|
||||
ElasticsearchDataQuery,
|
||||
} from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { defaultBucketAgg } from '../../../../queryDef';
|
||||
import { reducerTester } from '../../../reducerTester';
|
||||
import { changeMetricType } from '../../MetricAggregationsEditor/state/actions';
|
||||
|
||||
+7
-7
@@ -1,6 +1,7 @@
|
||||
import { Action } from '@reduxjs/toolkit';
|
||||
|
||||
import { BucketAggregation, ElasticsearchDataQuery, Terms } from '../../../../dataquery.gen';
|
||||
import { BucketAggregation, ElasticsearchDataQuery, Terms } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { defaultBucketAgg } from '../../../../queryDef';
|
||||
import { removeEmpty } from '../../../../utils';
|
||||
import { changeMetricType } from '../../MetricAggregationsEditor/state/actions';
|
||||
@@ -46,12 +47,11 @@ export const createReducer =
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: The previous version of the query editor was keeping some of the old bucket aggregation's configurations
|
||||
in the new selected one (such as field or some settings).
|
||||
It the future would be nice to have the same behavior but it's hard without a proper definition,
|
||||
as Elasticsearch will error sometimes if some settings are not compatible.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
TODO: The previous version of the query editor was keeping some of the old bucket aggregation's configurations
|
||||
in the new selected one (such as field or some settings).
|
||||
It the future would be nice to have the same behavior but it's hard without a proper definition,
|
||||
as Elasticsearch will error sometimes if some settings are not compatible.
|
||||
*/
|
||||
return {
|
||||
id: bucketAgg.id,
|
||||
type: action.payload.newType,
|
||||
|
||||
+4
-4
@@ -2,10 +2,10 @@ import { css } from '@emotion/css';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { Fragment, useEffect } from 'react';
|
||||
|
||||
import { InlineLabel, Input } from '@grafana/ui';
|
||||
import { Input, InlineLabel } from '@grafana/ui';
|
||||
import { BucketScript, MetricAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { BucketScript, MetricAggregation } from '../../../../../dataquery.gen';
|
||||
import { useDispatch, useStatelessReducer } from '../../../../../hooks/useStatelessReducer';
|
||||
import { useStatelessReducer, useDispatch } from '../../../../../hooks/useStatelessReducer';
|
||||
import { AddRemove } from '../../../../AddRemove';
|
||||
import { MetricPicker } from '../../../../MetricPicker';
|
||||
import { changeMetricAttribute } from '../../state/actions';
|
||||
@@ -13,9 +13,9 @@ import { SettingField } from '../SettingField';
|
||||
|
||||
import {
|
||||
addPipelineVariable,
|
||||
changePipelineVariableMetric,
|
||||
removePipelineVariable,
|
||||
renamePipelineVariable,
|
||||
changePipelineVariableMetric,
|
||||
} from './state/actions';
|
||||
import { reducer } from './state/reducer';
|
||||
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
import { PipelineVariable } from '../../../../../../dataquery.gen';
|
||||
import { PipelineVariable } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { reducerTester } from '../../../../../reducerTester';
|
||||
|
||||
import {
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { Action } from '@reduxjs/toolkit';
|
||||
|
||||
import { PipelineVariable } from '../../../../../../dataquery.gen';
|
||||
import { PipelineVariable } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { defaultPipelineVariable, generatePipelineVariableName } from '../utils';
|
||||
|
||||
import {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { PipelineVariable } from '../../../../../dataquery.gen';
|
||||
import { PipelineVariable } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
export const defaultPipelineVariable = (name: string): PipelineVariable => ({ name, pipelineAgg: '' });
|
||||
|
||||
|
||||
+4
-3
@@ -2,8 +2,11 @@ import { uniqueId } from 'lodash';
|
||||
import { ComponentProps, useState } from 'react';
|
||||
|
||||
import { InlineField, Input, TextArea } from '@grafana/ui';
|
||||
import {
|
||||
MetricAggregationWithSettings,
|
||||
MetricAggregationWithInlineScript,
|
||||
} from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { MetricAggregationWithInlineScript, MetricAggregationWithSettings } from '../../../../dataquery.gen';
|
||||
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
||||
import { getScriptValue } from '../../../../utils';
|
||||
import { SettingKeyOf } from '../../../types';
|
||||
@@ -30,11 +33,9 @@ export function SettingField<T extends MetricAggregationWithSettings, K extends
|
||||
const [id] = useState(uniqueId(`es-field-id-`));
|
||||
const settings = metric.settings;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
let defaultValue = settings?.[settingName as keyof typeof settings] || '';
|
||||
|
||||
if (settingName === 'script') {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
defaultValue = getScriptValue(metric as MetricAggregationWithInlineScript);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -2,8 +2,8 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { AsyncMultiSelect, InlineField, SegmentAsync, Select } from '@grafana/ui';
|
||||
import { TopMetrics } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { TopMetrics } from '../../../../dataquery.gen';
|
||||
import { useFields } from '../../../../hooks/useFields';
|
||||
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
||||
import { orderOptions } from '../../BucketAggregationsEditor/utils';
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import { getDefaultTimeRange } from '@grafana/data';
|
||||
import { ElasticsearchDataQuery } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { ElasticsearchDataQuery } from '../../../../dataquery.gen';
|
||||
import { ElasticDatasource } from '../../../../datasource';
|
||||
import { ElasticsearchProvider } from '../../ElasticsearchQueryContext';
|
||||
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
import { uniqueId } from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { ComponentProps, useId, useRef, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { InlineField, InlineSwitch, Input, Select } from '@grafana/ui';
|
||||
import { InlineField, Input, InlineSwitch, Select } from '@grafana/ui';
|
||||
import { MetricAggregation, ExtendedStat } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { ExtendedStat, MetricAggregation } from '../../../../dataquery.gen';
|
||||
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
||||
import { extendedStats } from '../../../../queryDef';
|
||||
import { SettingsEditorContainer } from '../../SettingsEditorContainer';
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
import { MetricAggregation } from '../../../../dataquery.gen';
|
||||
import { MetricAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { extendedStats } from '../../../../queryDef';
|
||||
|
||||
const hasValue = (value: string) => (object: { value: string }) => object.value === value;
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { MetricAggregation, MetricAggregationWithSettings } from '../../../../dataquery.gen';
|
||||
import { MetricAggregation, MetricAggregationWithSettings } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { MetricAggregationWithMeta } from '../../../../types';
|
||||
|
||||
export const addMetric = createAction<MetricAggregation['id']>('@metrics/add');
|
||||
|
||||
+7
-1
@@ -1,4 +1,10 @@
|
||||
import { Derivative, ElasticsearchDataQuery, ExtendedStats, MetricAggregation } from '../../../../dataquery.gen';
|
||||
import {
|
||||
MetricAggregation,
|
||||
ElasticsearchDataQuery,
|
||||
Derivative,
|
||||
ExtendedStats,
|
||||
} from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { defaultMetricAgg } from '../../../../queryDef';
|
||||
import { reducerTester } from '../../../reducerTester';
|
||||
import { changeEditorTypeAndResetQuery, initQuery } from '../../state';
|
||||
|
||||
+2
-2
@@ -1,6 +1,7 @@
|
||||
import { Action } from '@reduxjs/toolkit';
|
||||
|
||||
import { ElasticsearchDataQuery, MetricAggregation } from '../../../../dataquery.gen';
|
||||
import { ElasticsearchDataQuery, MetricAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { defaultMetricAgg, queryTypeToMetricType } from '../../../../queryDef';
|
||||
import { removeEmpty } from '../../../../utils';
|
||||
import { changeEditorTypeAndResetQuery, initQuery } from '../../state';
|
||||
@@ -56,7 +57,6 @@ export const reducer = (
|
||||
It the future would be nice to have the same behavior but it's hard without a proper definition,
|
||||
as Elasticsearch will error sometimes if some settings are not compatible.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return {
|
||||
id: metric.id,
|
||||
type: action.payload.type,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AnyAction } from '@reduxjs/toolkit';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { Action } from 'redux';
|
||||
|
||||
import { StoreState } from '../types/store';
|
||||
import { StoreState } from 'app/types/store';
|
||||
|
||||
type GrafanaReducer<S = StoreState, A extends Action = AnyAction> = (state: S, action: A) => S;
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { onUpdateDatasourceSecureJsonDataOption, updateDatasourcePluginResetOption } from '@grafana/data';
|
||||
import { InlineField, SecretInput } from '@grafana/ui';
|
||||
|
||||
import { Props } from './ConfigEditor';
|
||||
|
||||
export const ApiKeyConfig = (props: Props) => {
|
||||
const { options } = props;
|
||||
|
||||
return (
|
||||
<InlineField label="API Key" labelWidth={14} interactive tooltip={'API Key authentication'}>
|
||||
<SecretInput
|
||||
required
|
||||
id="config-editor-api-key"
|
||||
isConfigured={!!options.secureJsonFields?.apiKey}
|
||||
placeholder="Enter your API key"
|
||||
width={40}
|
||||
onReset={() => updateDatasourcePluginResetOption(props, 'apiKey')}
|
||||
onChange={onUpdateDatasourceSecureJsonDataOption(props, 'apiKey')}
|
||||
/>
|
||||
</InlineField>
|
||||
);
|
||||
};
|
||||
@@ -14,15 +14,14 @@ import {
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Alert, SecureSocksProxySettings, Divider, Stack } from '@grafana/ui';
|
||||
|
||||
import { ElasticsearchOptions, ElasticsearchSecureJsonData } from '../types';
|
||||
import { ElasticsearchOptions } from '../types';
|
||||
|
||||
import { ApiKeyConfig } from './ApiKeyConfig';
|
||||
import { DataLinks } from './DataLinks';
|
||||
import { ElasticDetails } from './ElasticDetails';
|
||||
import { LogsConfig } from './LogsConfig';
|
||||
import { coerceOptions, isValidOptions } from './utils';
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps<ElasticsearchOptions, ElasticsearchSecureJsonData>;
|
||||
export type Props = DataSourcePluginOptionsEditorProps<ElasticsearchOptions>;
|
||||
|
||||
export const ConfigEditor = (props: Props) => {
|
||||
const { options, onOptionsChange } = props;
|
||||
@@ -49,16 +48,6 @@ export const ConfigEditor = (props: Props) => {
|
||||
authProps.selectedMethod = options.jsonData.sigV4Auth ? 'custom-sigv4' : authProps.selectedMethod;
|
||||
}
|
||||
|
||||
authProps.customMethods = [
|
||||
{
|
||||
id: 'custom-api-key',
|
||||
label: 'API Key',
|
||||
description: 'API Key authentication',
|
||||
component: <ApiKeyConfig {...props} />,
|
||||
},
|
||||
];
|
||||
authProps.selectedMethod = options.jsonData.apiKeyAuth ? 'custom-api-key' : authProps.selectedMethod;
|
||||
|
||||
return (
|
||||
<>
|
||||
{options.access === 'direct' && (
|
||||
@@ -84,7 +73,6 @@ export const ConfigEditor = (props: Props) => {
|
||||
jsonData: {
|
||||
...options.jsonData,
|
||||
sigV4Auth: method === 'custom-sigv4',
|
||||
apiKeyAuth: method === 'custom-api-key',
|
||||
oauthPassThru: method === AuthMethod.OAuthForward,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
import '@grafana/plugin-configs/jest/jest-setup';
|
||||
@@ -1,3 +0,0 @@
|
||||
import defaultConfig from '@grafana/plugin-configs/jest/jest.config.js';
|
||||
|
||||
export default defaultConfig;
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"name": "@grafana-plugins/elasticsearch",
|
||||
"description": "Grafana data source for Elasticsearch",
|
||||
"private": true,
|
||||
"version": "12.4.0-pre",
|
||||
"dependencies": {
|
||||
"@emotion/css": "11.13.5",
|
||||
"@grafana/aws-sdk": "0.8.3",
|
||||
"@grafana/data": "12.4.0-pre",
|
||||
"@grafana/plugin-ui": "^0.11.1",
|
||||
"@grafana/runtime": "12.4.0-pre",
|
||||
"@grafana/schema": "12.4.0-pre",
|
||||
"@grafana/ui": "12.4.0-pre",
|
||||
"@reduxjs/toolkit": "2.10.1",
|
||||
"lodash": "4.17.21",
|
||||
"lucene": "^2.1.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-redux": "9.2.0",
|
||||
"react-select": "5.10.2",
|
||||
"react-use": "17.6.0",
|
||||
"redux": "5.0.1",
|
||||
"redux-thunk": "3.1.0",
|
||||
"rxjs": "7.8.2",
|
||||
"semver": "7.7.3",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grafana/e2e-selectors": "12.4.0-pre",
|
||||
"@grafana/plugin-configs": "12.4.0-pre",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/jest-dom": "6.6.4",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/lucene": "^2",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/semver": "7.7.1",
|
||||
"jest": "29.7.0",
|
||||
"react-select-event": "5.5.1",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.2",
|
||||
"webpack": "5.101.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@grafana/runtime": "*"
|
||||
},
|
||||
"resolutions": {
|
||||
"redux": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack -c ./webpack.config.ts --env production",
|
||||
"build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)",
|
||||
"dev": "webpack -w -c ./webpack.config.ts --env development",
|
||||
"test": "jest --watch --onlyChanged",
|
||||
"test:ci": "jest --maxWorkers 4"
|
||||
},
|
||||
"packageManager": "yarn@4.11.0"
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
"type": "datasource",
|
||||
"name": "Elasticsearch",
|
||||
"id": "elasticsearch",
|
||||
"executable": "gpx_elasticsearch",
|
||||
"category": "logging",
|
||||
"info": {
|
||||
"description": "Open source logging & analytics database",
|
||||
@@ -28,8 +27,7 @@
|
||||
"name": "Documentation",
|
||||
"url": "https://grafana.com/docs/grafana/latest/datasources/elasticsearch/"
|
||||
}
|
||||
],
|
||||
"version": "%VERSION%"
|
||||
]
|
||||
},
|
||||
"alerting": true,
|
||||
"annotations": true,
|
||||
@@ -38,9 +36,5 @@
|
||||
"backend": true,
|
||||
"queryOptions": {
|
||||
"minInterval": true
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": ">=11.6.0",
|
||||
"plugins": []
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user