Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f584d143f | |||
| e38240369b | |||
| 26ac64cf1a | |||
| ba6a783997 | |||
| d8777012cb | |||
| dfa07d8e10 | |||
| f704b8aa79 | |||
| dc63eb3314 | |||
| c1a46fdcb5 | |||
| 7143324229 | |||
| 1e3c6fed55 | |||
| d02201f564 | |||
| 2a9377ac02 |
@@ -121,6 +121,8 @@ linters:
|
||||
- '**/pkg/tsdb/zipkin/**/*'
|
||||
- '**/pkg/tsdb/jaeger/*'
|
||||
- '**/pkg/tsdb/jaeger/**/*'
|
||||
- '**/pkg/tsdb/elasticsearch/*'
|
||||
- '**/pkg/tsdb/elasticsearch/**/*'
|
||||
deny:
|
||||
- pkg: github.com/grafana/grafana/pkg/api
|
||||
desc: Core plugins are not allowed to depend on Grafana core packages
|
||||
|
||||
@@ -800,6 +800,8 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -804,6 +804,8 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -241,6 +241,8 @@ 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,6 +241,8 @@ 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,6 +804,8 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -1426,6 +1426,8 @@ 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,6 +5133,22 @@ 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,6 +808,8 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -1429,6 +1429,8 @@ 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,6 +5196,22 @@ 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,10 +103,11 @@ To configure basic settings for the data source, complete the following steps:
|
||||
|
||||
1. Set the data source's basic configuration options:
|
||||
|
||||
| Name | Description |
|
||||
| ----------- | ------------------------------------------------------------------------ |
|
||||
| **Name** | Sets the name you use to refer to the data source in panels and queries. |
|
||||
| **Default** | Sets whether the data source is pre-selected for new panels. |
|
||||
| Name | Description |
|
||||
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | Sets the name you use to refer to the data source in panels and queries. |
|
||||
| **Default** | Sets whether the data source is pre-selected for new panels. |
|
||||
| **Universe Domain** | The universe domain to connect to. For more information, refer to [Documentation on universe domains](https://docs.cloud.google.com/python/docs/reference/monitoring/latest/google.cloud.monitoring_v3.services.service_monitoring_service.ServiceMonitoringServiceAsyncClient#google_cloud_monitoring_v3_services_service_monitoring_service_ServiceMonitoringServiceAsyncClient_universe_domain). Defaults to `googleapis.com`. |
|
||||
|
||||
### Provision the data source
|
||||
|
||||
@@ -129,6 +130,7 @@ datasources:
|
||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||
authenticationType: jwt
|
||||
defaultProject: my-project-name
|
||||
universeDomain: googleapis.com
|
||||
secureJsonData:
|
||||
privateKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
@@ -152,6 +154,7 @@ datasources:
|
||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||
authenticationType: jwt
|
||||
defaultProject: my-project-name
|
||||
universeDomain: googleapis.com
|
||||
privateKeyPath: /etc/secrets/gce.pem
|
||||
```
|
||||
|
||||
@@ -166,6 +169,7 @@ datasources:
|
||||
access: proxy
|
||||
jsonData:
|
||||
authenticationType: gce
|
||||
universeDomain: googleapis.com
|
||||
```
|
||||
|
||||
## Import pre-configured dashboards
|
||||
|
||||
@@ -87,6 +87,7 @@ With a Grafana Enterprise license, you also get access to premium data sources,
|
||||
- [CockroachDB](/grafana/plugins/grafana-cockroachdb-datasource)
|
||||
- [Databricks](/grafana/plugins/grafana-databricks-datasource)
|
||||
- [DataDog](/grafana/plugins/grafana-datadog-datasource)
|
||||
- [IBM Db2](/grafana/plugins/grafana-ibmdb2-datasource)
|
||||
- [Drone](/grafana/plugins/grafana-drone-datasource)
|
||||
- [DynamoDB](/grafana/plugins/grafana-dynamodb-datasource/)
|
||||
- [Dynatrace](/grafana/plugins/grafana-dynatrace-datasource)
|
||||
|
||||
@@ -3743,46 +3743,21 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/aggregations.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/aggregations.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/elasticsearch/configuration/DataLinks.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
|
||||
@@ -82,6 +82,7 @@ module.exports = {
|
||||
// Decoupled plugins run their own tests so ignoring them here.
|
||||
'<rootDir>/public/app/plugins/datasource/azuremonitor',
|
||||
'<rootDir>/public/app/plugins/datasource/cloud-monitoring',
|
||||
'<rootDir>/public/app/plugins/datasource/elasticsearch',
|
||||
'<rootDir>/public/app/plugins/datasource/grafana-postgresql-datasource',
|
||||
'<rootDir>/public/app/plugins/datasource/grafana-pyroscope-datasource',
|
||||
'<rootDir>/public/app/plugins/datasource/grafana-testdata-datasource',
|
||||
|
||||
@@ -237,6 +237,8 @@ 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,6 +91,8 @@ 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 {
|
||||
@@ -118,6 +120,7 @@ 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;
|
||||
@@ -193,6 +196,7 @@ 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 = "12.4.0-pre";
|
||||
export const pluginVersion = "%VERSION%";
|
||||
|
||||
export type BucketAggregation = (DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested);
|
||||
|
||||
|
||||
@@ -231,6 +231,10 @@ 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,7 +715,9 @@ VariableOption: {
|
||||
// Text to be displayed for the option
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
+2
@@ -903,6 +903,8 @@ 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,6 +24,24 @@ func TestMain(m *testing.M) {
|
||||
testsuite.Run(m)
|
||||
}
|
||||
|
||||
// mockElasticsearchHandler returns a handler that mocks Elasticsearch endpoints.
|
||||
// It responds to GET / with cluster info (required for datasource initialization)
|
||||
// and returns 401 Unauthorized for all other requests.
|
||||
func mockElasticsearchHandler(onRequest func(r *http.Request)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"version":{"build_flavor":"default","number":"8.0.0"}}`))
|
||||
default:
|
||||
if onRequest != nil {
|
||||
onRequest(r)
|
||||
}
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationElasticsearch(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
@@ -35,9 +53,8 @@ func TestIntegrationElasticsearch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
var outgoingRequest *http.Request
|
||||
outgoingServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
outgoingServer := httptest.NewServer(mockElasticsearchHandler(func(r *http.Request) {
|
||||
outgoingRequest = r
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}))
|
||||
t.Cleanup(outgoingServer.Close)
|
||||
|
||||
|
||||
@@ -639,7 +639,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaDependency": "\u003e=11.6.0",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
|
||||
@@ -3912,6 +3912,13 @@
|
||||
"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,6 +3939,13 @@
|
||||
"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("%v/v3/projects/%v/metricDescriptors", dsInfo.services[cloudMonitor].url, defaultProject)
|
||||
url := fmt.Sprintf("%s/v3/projects/%s/metricDescriptors", dsInfo.services[cloudMonitor].url, defaultProject)
|
||||
request, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -139,6 +139,7 @@ type datasourceInfo struct {
|
||||
defaultProject string
|
||||
clientEmail string
|
||||
tokenUri string
|
||||
universeDomain string
|
||||
services map[string]datasourceService
|
||||
privateKey string
|
||||
usingImpersonation bool
|
||||
@@ -150,6 +151,7 @@ type datasourceJSONData struct {
|
||||
DefaultProject string `json:"defaultProject"`
|
||||
ClientEmail string `json:"clientEmail"`
|
||||
TokenURI string `json:"tokenUri"`
|
||||
UniverseDomain string `json:"universeDomain"`
|
||||
UsingImpersonation bool `json:"usingImpersonation"`
|
||||
ServiceAccountToImpersonate string `json:"serviceAccountToImpersonate"`
|
||||
}
|
||||
@@ -179,6 +181,7 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
|
||||
defaultProject: jsonData.DefaultProject,
|
||||
clientEmail: jsonData.ClientEmail,
|
||||
tokenUri: jsonData.TokenURI,
|
||||
universeDomain: jsonData.UniverseDomain,
|
||||
usingImpersonation: jsonData.UsingImpersonation,
|
||||
serviceAccountToImpersonate: jsonData.ServiceAccountToImpersonate,
|
||||
services: map[string]datasourceService{},
|
||||
@@ -194,13 +197,13 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for name, info := range routes {
|
||||
for name := range routes {
|
||||
client, err := newHTTPClient(dsInfo, opts, &httpClientProvider, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dsInfo.services[name] = datasourceService{
|
||||
url: info.url,
|
||||
url: buildURL(name, dsInfo.universeDomain),
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ type routeInfo struct {
|
||||
var routes = map[string]routeInfo{
|
||||
cloudMonitor: {
|
||||
method: "GET",
|
||||
url: "https://monitoring.googleapis.com",
|
||||
url: "https://monitoring.",
|
||||
scopes: []string{cloudMonitorScope},
|
||||
},
|
||||
resourceManager: {
|
||||
method: "GET",
|
||||
url: "https://cloudresourcemanager.googleapis.com",
|
||||
url: "https://cloudresourcemanager.",
|
||||
scopes: []string{resourceManagerScope},
|
||||
},
|
||||
}
|
||||
@@ -68,6 +68,13 @@ func getMiddleware(model *datasourceInfo, routePath string) (httpclient.Middlewa
|
||||
return tokenprovider.AuthMiddleware(provider), nil
|
||||
}
|
||||
|
||||
func buildURL(route string, universeDomain string) string {
|
||||
if universeDomain == "" {
|
||||
universeDomain = "googleapis.com"
|
||||
}
|
||||
return routes[route].url + universeDomain
|
||||
}
|
||||
|
||||
func newHTTPClient(model *datasourceInfo, opts httpclient.Options, clientProvider *httpclient.Provider, route string) (*http.Client, error) {
|
||||
m, err := getMiddleware(model, route)
|
||||
if err != nil {
|
||||
|
||||
@@ -111,7 +111,7 @@ func Test_setRequestVariables(t *testing.T) {
|
||||
im: &fakeInstance{
|
||||
services: map[string]datasourceService{
|
||||
cloudMonitor: {
|
||||
url: routes[cloudMonitor].url,
|
||||
url: buildURL(cloudMonitor, "googleapis.com"),
|
||||
client: &http.Client{},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,8 +3,8 @@ package elasticsearch
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
// addDateHistogramAgg adds a date histogram aggregation to the aggregation builder
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
// Used in logging to mark a stage
|
||||
@@ -35,6 +34,7 @@ type DatasourceInfo struct {
|
||||
Interval string
|
||||
MaxConcurrentShardRequests int64
|
||||
IncludeFrozen bool
|
||||
ClusterInfo ClusterInfo
|
||||
}
|
||||
|
||||
type ConfiguredFields struct {
|
||||
@@ -159,7 +159,7 @@ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearch
|
||||
resSpan.End()
|
||||
}()
|
||||
|
||||
improvedParsingEnabled := isFeatureEnabled(c.ctx, featuremgmt.FlagElasticsearchImprovedParsing)
|
||||
improvedParsingEnabled := isFeatureEnabled(c.ctx, "elasticsearchImprovedParsing")
|
||||
msr, err := c.parser.parseMultiSearchResponse(res.Body, improvedParsingEnabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -197,7 +197,11 @@ func (c *baseClientImpl) createMultiSearchRequests(searchRequests []*SearchReque
|
||||
|
||||
func (c *baseClientImpl) getMultiSearchQueryParameters() string {
|
||||
var qs []string
|
||||
qs = append(qs, fmt.Sprintf("max_concurrent_shard_requests=%d", c.ds.MaxConcurrentShardRequests))
|
||||
// if the build flavor is not serverless, we can use the max concurrent shard requests
|
||||
// this is because serverless clusters do not support max concurrent shard requests
|
||||
if !c.ds.ClusterInfo.IsServerless() && c.ds.MaxConcurrentShardRequests > 0 {
|
||||
qs = append(qs, fmt.Sprintf("max_concurrent_shard_requests=%d", c.ds.MaxConcurrentShardRequests))
|
||||
}
|
||||
|
||||
if c.ds.IncludeFrozen {
|
||||
qs = append(qs, "ignore_throttled=false")
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
func TestClient_ExecuteMultisearch(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package es
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type VersionInfo struct {
|
||||
BuildFlavor string `json:"build_flavor"`
|
||||
}
|
||||
|
||||
// ClusterInfo represents Elasticsearch cluster information returned from the root endpoint.
|
||||
// It is used to determine cluster capabilities and configuration like whether the cluster is serverless.
|
||||
type ClusterInfo struct {
|
||||
Version VersionInfo `json:"version"`
|
||||
}
|
||||
|
||||
const (
|
||||
BuildFlavorServerless = "serverless"
|
||||
)
|
||||
|
||||
// GetClusterInfo fetches cluster information from the Elasticsearch root endpoint.
|
||||
// It returns the cluster build flavor which is used to determine if the cluster is serverless.
|
||||
func GetClusterInfo(httpCli *http.Client, url string) (clusterInfo ClusterInfo, err error) {
|
||||
resp, err := httpCli.Get(url)
|
||||
if err != nil {
|
||||
return ClusterInfo{}, fmt.Errorf("error getting ES cluster info: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return ClusterInfo{}, fmt.Errorf("unexpected status code %d getting ES cluster info", resp.StatusCode)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if closeErr := resp.Body.Close(); closeErr != nil && err == nil {
|
||||
err = fmt.Errorf("error closing response body: %w", closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&clusterInfo)
|
||||
if err != nil {
|
||||
return ClusterInfo{}, fmt.Errorf("error decoding ES cluster info: %w", err)
|
||||
}
|
||||
|
||||
return clusterInfo, nil
|
||||
}
|
||||
|
||||
func (ci ClusterInfo) IsServerless() bool {
|
||||
return ci.Version.BuildFlavor == BuildFlavorServerless
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package es
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetClusterInfo(t *testing.T) {
|
||||
t.Run("Should successfully get cluster info", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
_, err := rw.Write([]byte(`{
|
||||
"name": "test-cluster",
|
||||
"cluster_name": "elasticsearch",
|
||||
"cluster_uuid": "abc123",
|
||||
"version": {
|
||||
"number": "8.0.0",
|
||||
"build_flavor": "default",
|
||||
"build_type": "tar",
|
||||
"build_hash": "abc123",
|
||||
"build_date": "2023-01-01T00:00:00.000Z",
|
||||
"build_snapshot": false,
|
||||
"lucene_version": "9.0.0"
|
||||
}
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, clusterInfo)
|
||||
assert.Equal(t, "default", clusterInfo.Version.BuildFlavor)
|
||||
})
|
||||
|
||||
t.Run("Should successfully get serverless cluster info", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
_, err := rw.Write([]byte(`{
|
||||
"name": "serverless-cluster",
|
||||
"cluster_name": "elasticsearch",
|
||||
"cluster_uuid": "def456",
|
||||
"version": {
|
||||
"number": "8.11.0",
|
||||
"build_flavor": "serverless",
|
||||
"build_type": "docker",
|
||||
"build_hash": "def456",
|
||||
"build_date": "2023-11-01T00:00:00.000Z",
|
||||
"build_snapshot": false,
|
||||
"lucene_version": "9.8.0"
|
||||
}
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, clusterInfo)
|
||||
assert.Equal(t, "serverless", clusterInfo.Version.BuildFlavor)
|
||||
assert.True(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should return error when HTTP request fails", func(t *testing.T) {
|
||||
clusterInfo, err := GetClusterInfo(http.DefaultClient, "http://invalid-url-that-does-not-exist.local:9999")
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ClusterInfo{}, clusterInfo)
|
||||
assert.Contains(t, err.Error(), "error getting ES cluster info")
|
||||
})
|
||||
|
||||
t.Run("Should return error when response body is invalid JSON", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
_, err := rw.Write([]byte(`{"invalid json`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ClusterInfo{}, clusterInfo)
|
||||
assert.Contains(t, err.Error(), "error decoding ES cluster info")
|
||||
})
|
||||
|
||||
t.Run("Should handle empty version object", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
_, err := rw.Write([]byte(`{
|
||||
"name": "test-cluster",
|
||||
"version": {}
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ClusterInfo{}, clusterInfo)
|
||||
assert.Equal(t, "", clusterInfo.Version.BuildFlavor)
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should handle HTTP error status codes", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
_, err := rw.Write([]byte(`{"error": "Unauthorized"}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
clusterInfo, err := GetClusterInfo(ts.Client(), ts.URL)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ClusterInfo{}, clusterInfo)
|
||||
assert.Contains(t, err.Error(), "unexpected status code 401 getting ES cluster info")
|
||||
})
|
||||
}
|
||||
|
||||
func TestClusterInfo_IsServerless(t *testing.T) {
|
||||
t.Run("Should return true when build_flavor is serverless", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{
|
||||
Version: VersionInfo{
|
||||
BuildFlavor: BuildFlavorServerless,
|
||||
},
|
||||
}
|
||||
|
||||
assert.True(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should return false when build_flavor is default", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{
|
||||
Version: VersionInfo{
|
||||
BuildFlavor: "default",
|
||||
},
|
||||
}
|
||||
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should return false when build_flavor is empty", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{
|
||||
Version: VersionInfo{
|
||||
BuildFlavor: "",
|
||||
},
|
||||
}
|
||||
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("Should return false when build_flavor is unknown value", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{
|
||||
Version: VersionInfo{
|
||||
BuildFlavor: "unknown",
|
||||
},
|
||||
}
|
||||
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
|
||||
t.Run("should return false when cluster info is empty", func(t *testing.T) {
|
||||
clusterInfo := ClusterInfo{}
|
||||
assert.False(t, clusterInfo.IsServerless())
|
||||
})
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
func TestSearchRequest(t *testing.T) {
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
// processQuery processes a single query and adds it to the multi-search request builder
|
||||
|
||||
@@ -3,7 +3,7 @@ package elasticsearch
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
// setFloatPath converts a string value at the specified path to float64
|
||||
|
||||
@@ -88,6 +88,14 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins
|
||||
httpCliOpts.SigV4.Service = "es"
|
||||
}
|
||||
|
||||
apiKeyAuth, ok := jsonData["apiKeyAuth"].(bool)
|
||||
if ok && apiKeyAuth {
|
||||
apiKey := settings.DecryptedSecureJSONData["apiKey"]
|
||||
if apiKey != "" {
|
||||
httpCliOpts.Header.Add("Authorization", "ApiKey "+apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
httpCli, err := httpClientProvider.New(httpCliOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -151,6 +159,11 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins
|
||||
includeFrozen = false
|
||||
}
|
||||
|
||||
clusterInfo, err := es.GetClusterInfo(httpCli, settings.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configuredFields := es.ConfiguredFields{
|
||||
TimeField: timeField,
|
||||
LogLevelField: logLevelField,
|
||||
@@ -166,6 +179,7 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins
|
||||
ConfiguredFields: configuredFields,
|
||||
Interval: interval,
|
||||
IncludeFrozen: includeFrozen,
|
||||
ClusterInfo: clusterInfo,
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package elasticsearch
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
@@ -18,8 +20,26 @@ type datasourceInfo struct {
|
||||
Interval string `json:"interval"`
|
||||
}
|
||||
|
||||
// mockElasticsearchServer creates a test HTTP server that mocks Elasticsearch cluster info endpoint
|
||||
func mockElasticsearchServer() *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// Return a mock Elasticsearch cluster info response
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"version": map[string]interface{}{
|
||||
"build_flavor": "serverless",
|
||||
"number": "8.0.0",
|
||||
},
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
func TestNewInstanceSettings(t *testing.T) {
|
||||
t.Run("fields exist", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: 5,
|
||||
@@ -28,6 +48,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -37,6 +58,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
|
||||
t.Run("timeField", func(t *testing.T) {
|
||||
t.Run("is nil", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
MaxConcurrentShardRequests: 5,
|
||||
Interval: "Daily",
|
||||
@@ -46,6 +70,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -54,6 +79,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("is empty", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
MaxConcurrentShardRequests: 5,
|
||||
Interval: "Daily",
|
||||
@@ -64,6 +92,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -74,6 +103,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
|
||||
t.Run("maxConcurrentShardRequests", func(t *testing.T) {
|
||||
t.Run("no maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
}
|
||||
@@ -81,6 +113,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -90,6 +123,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("string maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: "10",
|
||||
@@ -98,6 +134,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -107,6 +144,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("number maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: 10,
|
||||
@@ -115,6 +155,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -124,6 +165,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("zero maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: 0,
|
||||
@@ -132,6 +176,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -141,6 +186,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("negative maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: -10,
|
||||
@@ -149,6 +197,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -158,6 +207,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("float maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: 10.5,
|
||||
@@ -166,6 +218,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -175,6 +228,9 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("invalid maxConcurrentShardRequests", func(t *testing.T) {
|
||||
server := mockElasticsearchServer()
|
||||
defer server.Close()
|
||||
|
||||
dsInfo := datasourceInfo{
|
||||
TimeField: "@timestamp",
|
||||
MaxConcurrentShardRequests: "invalid",
|
||||
@@ -183,6 +239,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
dsSettings := backend.DataSourceInstanceSettings{
|
||||
URL: server.URL,
|
||||
JSONData: json.RawMessage(settingsJSON),
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
|
||||
Message: "Health check failed: Failed to get data source info",
|
||||
}, nil
|
||||
}
|
||||
|
||||
healthStatusUrl, err := url.Parse(ds.URL)
|
||||
if err != nil {
|
||||
logger.Error("Failed to parse data source URL", "error", err)
|
||||
@@ -38,6 +37,14 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
|
||||
}, nil
|
||||
}
|
||||
|
||||
// If the cluster is serverless, return a healthy result
|
||||
if ds.ClusterInfo.IsServerless() {
|
||||
return &backend.CheckHealthResult{
|
||||
Status: backend.HealthStatusOk,
|
||||
Message: "Elasticsearch Serverless data source is healthy.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// check that ES is healthy
|
||||
healthStatusUrl.Path = path.Join(healthStatusUrl.Path, "_cluster/health")
|
||||
healthStatusUrl.RawQuery = "wait_for_status=yellow"
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
// metricsResponseProcessor handles processing of metrics query responses
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/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/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/simplejson"
|
||||
)
|
||||
|
||||
func parseQuery(tsdbQuery []backend.DataQuery, logger log.Logger) ([]*Query, error) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/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.
|
||||
|
||||
@@ -0,0 +1,582 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
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")
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
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(`{`))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -284,6 +284,7 @@ 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,7 +69,6 @@ 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');
|
||||
@@ -125,7 +124,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
|
||||
|
||||
{EditorToRender && <EditorToRender variable={variable} onRunQuery={onRunQuery} />}
|
||||
|
||||
{isHasVariableOptions && <VariableValuesPreview options={optionsForSelect} hasMultiProps={hasMultiProps} />}
|
||||
{isHasVariableOptions && <VariableValuesPreview options={optionsForSelect} />}
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<Stack gap={2}>
|
||||
|
||||
+8
-5
@@ -10,13 +10,16 @@ import { Button, InlineFieldRow, InlineLabel, InteractiveTable, Text, useStyles2
|
||||
|
||||
export interface Props {
|
||||
options: VariableValueOption[];
|
||||
hasMultiProps?: boolean;
|
||||
}
|
||||
|
||||
export const VariableValuesPreview = ({ options, hasMultiProps }: Props) => {
|
||||
const hasMultiProps = (options: Props['options']) => {
|
||||
return Object.keys(options[1]?.properties ?? options[0]?.properties ?? {}).length > 0;
|
||||
};
|
||||
|
||||
export const VariableValuesPreview = ({ options }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const hasOptions = options.length > 0;
|
||||
const displayMultiPropsPreview = config.featureToggles.multiPropsVariables && hasMultiProps;
|
||||
const displayMultiPropsPreview = config.featureToggles.multiPropsVariables && hasOptions && hasMultiProps(options);
|
||||
|
||||
return (
|
||||
<div className={styles.previewContainer} style={{ gap: '8px' }}>
|
||||
@@ -43,7 +46,8 @@ function VariableValuesWithPropsPreview({ options }: { options: VariableValueOpt
|
||||
|
||||
return {
|
||||
data,
|
||||
columns: Object.keys(data[0] ?? {}).map((id) => ({
|
||||
// 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) => ({
|
||||
id,
|
||||
// see https://github.com/TanStack/table/issues/1671
|
||||
header: unsanitizeKey(id),
|
||||
@@ -62,7 +66,6 @@ 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} hasMultiProps={valuesFormat === 'json'} />
|
||||
<VariableValuesPreview options={options} />
|
||||
</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(31);
|
||||
expect(enterprisePluginsCategory.plugins.length).toBe(32);
|
||||
expect(enterprisePluginsCategory.plugins[0].name).toBe('Adobe Analytics');
|
||||
expect(enterprisePluginsCategory.plugins[enterprisePluginsCategory.plugins.length - 1].name).toBe('Zendesk');
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ 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';
|
||||
@@ -418,6 +419,12 @@ 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,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -81,16 +81,17 @@ const buildLabelPath = (label: string) => {
|
||||
};
|
||||
|
||||
const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
|
||||
if (!('valuesFormat' in variable) || variable.valuesFormat !== 'json') {
|
||||
if (!('options' in variable) || !variable.options[0].properties) {
|
||||
return [];
|
||||
}
|
||||
|
||||
function collectFieldPaths(option: Record<string, string>, currentPath: string) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function collectFieldPaths(properties: Record<string, any>, currentPath: string) {
|
||||
let paths: string[] = [];
|
||||
for (const field in option) {
|
||||
if (option.hasOwnProperty(field)) {
|
||||
for (const field in properties) {
|
||||
if (properties.hasOwnProperty(field)) {
|
||||
const newPath = `${currentPath}.${field}`;
|
||||
const value = option[field];
|
||||
const value = properties[field];
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
paths = [...paths, ...collectFieldPaths(value, newPath)];
|
||||
}
|
||||
@@ -100,11 +101,7 @@ const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
|
||||
return paths;
|
||||
}
|
||||
|
||||
try {
|
||||
return collectFieldPaths(JSON.parse(variable.query)[0], variable.name);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return collectFieldPaths(variable.options[0].properties, variable.name);
|
||||
};
|
||||
|
||||
export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [
|
||||
|
||||
@@ -503,13 +503,16 @@ describe('linkSrv', () => {
|
||||
});
|
||||
|
||||
describe('getPanelLinksVariableSuggestions', () => {
|
||||
it('then it should return template variables, json properties and built-ins', () => {
|
||||
it('then it should return template variables, options properties and built-ins', () => {
|
||||
const templateSrvWithJsonValues = initTemplateSrv('key', [
|
||||
{
|
||||
type: 'custom',
|
||||
name: 'customServers',
|
||||
valuesFormat: 'json',
|
||||
query: '[{"name":"web","ip":"192.168.0.100"},{"name":"ads","ip":"192.168.0.142"}]',
|
||||
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' } },
|
||||
],
|
||||
},
|
||||
]);
|
||||
setTemplateSrv(templateSrvWithJsonValues);
|
||||
|
||||
@@ -4,8 +4,6 @@ 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 () =>
|
||||
@@ -75,7 +73,6 @@ 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,
|
||||
|
||||
+25
-6
@@ -1,10 +1,10 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
||||
import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data';
|
||||
import { ConnectionConfig } from '@grafana/google-sdk';
|
||||
import { ConfigSection, DataSourceDescription } from '@grafana/plugin-ui';
|
||||
import { reportInteraction, config } from '@grafana/runtime';
|
||||
import { Divider, SecureSocksProxySettings } from '@grafana/ui';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { Divider, Field, Input, SecureSocksProxySettings, Stack } from '@grafana/ui';
|
||||
|
||||
import { CloudMonitoringOptions, CloudMonitoringSecureJsonData } from '../../types/types';
|
||||
|
||||
@@ -36,14 +36,33 @@ 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."
|
||||
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."
|
||||
isCollapsible
|
||||
isInitiallyOpen={options.jsonData.enableSecureSocksProxy !== undefined}
|
||||
isInitiallyOpen={
|
||||
options.jsonData.enableSecureSocksProxy !== undefined || options.jsonData.universeDomain !== undefined
|
||||
}
|
||||
>
|
||||
<SecureSocksProxySettings options={options} onOptionsChange={onOptionsChange} />
|
||||
<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>
|
||||
</ConfigSection>
|
||||
</>
|
||||
)}
|
||||
<Divider />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface Aggregation {
|
||||
export interface CloudMonitoringOptions extends DataSourceOptions {
|
||||
gceDefaultProject?: string;
|
||||
enableSecureSocksProxy?: boolean;
|
||||
universeDomain?: string;
|
||||
}
|
||||
|
||||
export interface CloudMonitoringSecureJsonData extends DataSourceSecureJsonData {}
|
||||
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { select } from 'react-select-event';
|
||||
|
||||
import { DateHistogram } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { DateHistogram } from '../../../../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<unknown, GroupBase<unknown>>
|
||||
options: OptionsOrGroups<SelectableValue<string>, GroupBase<SelectableValue<string>>>
|
||||
) => {
|
||||
// TODO: would be extremely nice here to allow only template variables and values that are
|
||||
// valid date histogram's Interval options
|
||||
const valueExists = (options as Array<SelectableValue<string>>).some(hasValue(inputValue));
|
||||
const valueExists = options.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 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
import { Filter } from '../../../../../../dataquery.gen';
|
||||
|
||||
export const addFilter = createAction('@bucketAggregations/filter/add');
|
||||
export const removeFilter = createAction<number>('@bucketAggregations/filter/remove');
|
||||
|
||||
+2
-3
@@ -1,6 +1,5 @@
|
||||
import { reducerTester } from 'test/core/redux/reducerTester';
|
||||
|
||||
import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
import { Filter } from '../../../../../../dataquery.gen';
|
||||
import { reducerTester } from '../../../../../reducerTester';
|
||||
|
||||
import { addFilter, changeFilter, removeFilter } from './actions';
|
||||
import { reducer } from './reducer';
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
import { Action } from 'redux';
|
||||
|
||||
import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { Filter } from '../../../../../../dataquery.gen';
|
||||
import { defaultFilter } from '../utils';
|
||||
|
||||
import { addFilter, changeFilter, removeFilter } from './actions';
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
import { Filter } from '../../../../../dataquery.gen';
|
||||
|
||||
export const defaultFilter = (): Filter => ({ label: '', query: '*' });
|
||||
|
||||
+1
-8
@@ -1,14 +1,7 @@
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import selectEvent from 'react-select-event';
|
||||
|
||||
import {
|
||||
Average,
|
||||
Derivative,
|
||||
ElasticsearchDataQuery,
|
||||
Terms,
|
||||
TopMetrics,
|
||||
} from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { Average, Derivative, ElasticsearchDataQuery, Terms, TopMetrics } from '../../../../dataquery.gen';
|
||||
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
||||
import { renderWithESProvider } from '../../../../test-helpers/render';
|
||||
import { describeMetric } from '../../../../utils';
|
||||
|
||||
+3
-9
@@ -2,15 +2,9 @@ import { uniqueId } from 'lodash';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { InlineField, Select, Input } from '@grafana/ui';
|
||||
import {
|
||||
Terms,
|
||||
ExtendedStats,
|
||||
ExtendedStatMetaType,
|
||||
Percentiles,
|
||||
MetricAggregation,
|
||||
} from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
import { InlineField, Input, Select } from '@grafana/ui';
|
||||
|
||||
import { ExtendedStats, MetricAggregation, Percentiles, Terms } from '../../../../dataquery.gen';
|
||||
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
||||
import { describeMetric } from '../../../../utils';
|
||||
import { useQuery } from '../../ElasticsearchQueryContext';
|
||||
@@ -105,7 +99,7 @@ function createOrderByOptionsForExtendedStats(metric: ExtendedStats): Selectable
|
||||
if (!metric.meta) {
|
||||
return [];
|
||||
}
|
||||
const metaKeys = Object.keys(metric.meta) as ExtendedStatMetaType[];
|
||||
const metaKeys = Object.keys(metric.meta);
|
||||
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';
|
||||
|
||||
+2
-3
@@ -1,7 +1,6 @@
|
||||
import { BucketAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { BucketAggregation } from '../../../../dataquery.gen';
|
||||
import { defaultGeoHashPrecisionString } from '../../../../queryDef';
|
||||
import { describeMetric, convertOrderByToMetricId } from '../../../../utils';
|
||||
import { convertOrderByToMetricId, describeMetric } from '../../../../utils';
|
||||
import { useQuery } from '../../ElasticsearchQueryContext';
|
||||
import { bucketAggregationConfig, orderByOptions, orderOptions } from '../utils';
|
||||
|
||||
|
||||
+1
-5
@@ -1,10 +1,6 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import {
|
||||
BucketAggregation,
|
||||
BucketAggregationType,
|
||||
BucketAggregationWithField,
|
||||
} from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
import { BucketAggregation, BucketAggregationType, BucketAggregationWithField } from '../../../../dataquery.gen';
|
||||
|
||||
export const addBucketAggregation = createAction<BucketAggregation['id']>('@bucketAggs/add');
|
||||
export const removeBucketAggregation = createAction<BucketAggregation['id']>('@bucketAggs/remove');
|
||||
|
||||
+1
-6
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
BucketAggregation,
|
||||
DateHistogram,
|
||||
ElasticsearchDataQuery,
|
||||
} from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { BucketAggregation, DateHistogram, ElasticsearchDataQuery } from '../../../../dataquery.gen';
|
||||
import { defaultBucketAgg } from '../../../../queryDef';
|
||||
import { reducerTester } from '../../../reducerTester';
|
||||
import { changeMetricType } from '../../MetricAggregationsEditor/state/actions';
|
||||
|
||||
+7
-7
@@ -1,7 +1,6 @@
|
||||
import { Action } from '@reduxjs/toolkit';
|
||||
|
||||
import { BucketAggregation, ElasticsearchDataQuery, Terms } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { BucketAggregation, ElasticsearchDataQuery, Terms } from '../../../../dataquery.gen';
|
||||
import { defaultBucketAgg } from '../../../../queryDef';
|
||||
import { removeEmpty } from '../../../../utils';
|
||||
import { changeMetricType } from '../../MetricAggregationsEditor/state/actions';
|
||||
@@ -47,11 +46,12 @@ 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.
|
||||
*/
|
||||
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
|
||||
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 { Input, InlineLabel } from '@grafana/ui';
|
||||
import { BucketScript, MetricAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
import { InlineLabel, Input } from '@grafana/ui';
|
||||
|
||||
import { useStatelessReducer, useDispatch } from '../../../../../hooks/useStatelessReducer';
|
||||
import { BucketScript, MetricAggregation } from '../../../../../dataquery.gen';
|
||||
import { useDispatch, useStatelessReducer } 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';
|
||||
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
import { PipelineVariable } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { PipelineVariable } from '../../../../../../dataquery.gen';
|
||||
import { reducerTester } from '../../../../../reducerTester';
|
||||
|
||||
import {
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
import { Action } from '@reduxjs/toolkit';
|
||||
|
||||
import { PipelineVariable } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { PipelineVariable } from '../../../../../../dataquery.gen';
|
||||
import { defaultPipelineVariable, generatePipelineVariableName } from '../utils';
|
||||
|
||||
import {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { PipelineVariable } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
import { PipelineVariable } from '../../../../../dataquery.gen';
|
||||
|
||||
export const defaultPipelineVariable = (name: string): PipelineVariable => ({ name, pipelineAgg: '' });
|
||||
|
||||
|
||||
+3
-4
@@ -2,11 +2,8 @@ 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';
|
||||
@@ -33,9 +30,11 @@ 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 { ComponentProps, useId, useRef, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
import { ComponentProps, useId, useRef, useState } from 'react';
|
||||
|
||||
import { InlineField, Input, InlineSwitch, Select } from '@grafana/ui';
|
||||
import { MetricAggregation, ExtendedStat } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
import { InlineField, InlineSwitch, Input, Select } from '@grafana/ui';
|
||||
|
||||
import { ExtendedStat, MetricAggregation } from '../../../../dataquery.gen';
|
||||
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
||||
import { extendedStats } from '../../../../queryDef';
|
||||
import { SettingsEditorContainer } from '../../SettingsEditorContainer';
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
import { MetricAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { MetricAggregation } from '../../../../dataquery.gen';
|
||||
import { extendedStats } from '../../../../queryDef';
|
||||
|
||||
const hasValue = (value: string) => (object: { value: string }) => object.value === value;
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { MetricAggregation, MetricAggregationWithSettings } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { MetricAggregation, MetricAggregationWithSettings } from '../../../../dataquery.gen';
|
||||
import { MetricAggregationWithMeta } from '../../../../types';
|
||||
|
||||
export const addMetric = createAction<MetricAggregation['id']>('@metrics/add');
|
||||
|
||||
+1
-7
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
MetricAggregation,
|
||||
ElasticsearchDataQuery,
|
||||
Derivative,
|
||||
ExtendedStats,
|
||||
} from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { Derivative, ElasticsearchDataQuery, ExtendedStats, MetricAggregation } from '../../../../dataquery.gen';
|
||||
import { defaultMetricAgg } from '../../../../queryDef';
|
||||
import { reducerTester } from '../../../reducerTester';
|
||||
import { changeEditorTypeAndResetQuery, initQuery } from '../../state';
|
||||
|
||||
+2
-2
@@ -1,7 +1,6 @@
|
||||
import { Action } from '@reduxjs/toolkit';
|
||||
|
||||
import { ElasticsearchDataQuery, MetricAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen';
|
||||
|
||||
import { ElasticsearchDataQuery, MetricAggregation } from '../../../../dataquery.gen';
|
||||
import { defaultMetricAgg, queryTypeToMetricType } from '../../../../queryDef';
|
||||
import { removeEmpty } from '../../../../utils';
|
||||
import { changeEditorTypeAndResetQuery, initQuery } from '../../state';
|
||||
@@ -57,6 +56,7 @@ 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 'app/types/store';
|
||||
import { StoreState } from '../types/store';
|
||||
|
||||
type GrafanaReducer<S = StoreState, A extends Action = AnyAction> = (state: S, action: A) => S;
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
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,14 +14,15 @@ import {
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Alert, SecureSocksProxySettings, Divider, Stack } from '@grafana/ui';
|
||||
|
||||
import { ElasticsearchOptions } from '../types';
|
||||
import { ElasticsearchOptions, ElasticsearchSecureJsonData } 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>;
|
||||
export type Props = DataSourcePluginOptionsEditorProps<ElasticsearchOptions, ElasticsearchSecureJsonData>;
|
||||
|
||||
export const ConfigEditor = (props: Props) => {
|
||||
const { options, onOptionsChange } = props;
|
||||
@@ -48,6 +49,16 @@ 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' && (
|
||||
@@ -73,6 +84,7 @@ export const ConfigEditor = (props: Props) => {
|
||||
jsonData: {
|
||||
...options.jsonData,
|
||||
sigV4Auth: method === 'custom-sigv4',
|
||||
apiKeyAuth: method === 'custom-api-key',
|
||||
oauthPassThru: method === AuthMethod.OAuthForward,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import '@grafana/plugin-configs/jest/jest-setup';
|
||||
@@ -0,0 +1,3 @@
|
||||
import defaultConfig from '@grafana/plugin-configs/jest/jest.config.js';
|
||||
|
||||
export default defaultConfig;
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"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,6 +2,7 @@
|
||||
"type": "datasource",
|
||||
"name": "Elasticsearch",
|
||||
"id": "elasticsearch",
|
||||
"executable": "gpx_elasticsearch",
|
||||
"category": "logging",
|
||||
"info": {
|
||||
"description": "Open source logging & analytics database",
|
||||
@@ -27,7 +28,8 @@
|
||||
"name": "Documentation",
|
||||
"url": "https://grafana.com/docs/grafana/latest/datasources/elasticsearch/"
|
||||
}
|
||||
]
|
||||
],
|
||||
"version": "%VERSION%"
|
||||
},
|
||||
"alerting": true,
|
||||
"annotations": true,
|
||||
@@ -36,5 +38,9 @@
|
||||
"backend": true,
|
||||
"queryOptions": {
|
||||
"minInterval": true
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": ">=11.6.0",
|
||||
"plugins": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "../../../../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:plugin", "type:datasource"],
|
||||
"targets": {
|
||||
"build": {},
|
||||
"dev": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { StoreState } from '../../types/store';
|
||||
|
||||
export type CleanUpAction = (state: StoreState) => void;
|
||||
|
||||
export interface CleanUpPayload {
|
||||
cleanupAction: CleanUpAction;
|
||||
}
|
||||
|
||||
export const cleanUpAction = createAction<CleanUpPayload>('core/cleanUpState');
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ReducersMapObject } from '@reduxjs/toolkit';
|
||||
import { Action as AnyAction, combineReducers } from 'redux';
|
||||
|
||||
const addedReducers = {
|
||||
defaultReducer: (state = {}) => state,
|
||||
templating: (state = { lastKey: 'key' }) => state,
|
||||
};
|
||||
|
||||
export const addReducer = (newReducers: ReducersMapObject) => {
|
||||
Object.assign(addedReducers, newReducers);
|
||||
};
|
||||
|
||||
export const createRootReducer = () => {
|
||||
const appReducer = combineReducers({
|
||||
...addedReducers,
|
||||
});
|
||||
|
||||
return (state: Parameters<typeof appReducer>[0], action: AnyAction) => {
|
||||
return appReducer(state, action);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { createListenerMiddleware, configureStore as reduxConfigureStore } from '@reduxjs/toolkit';
|
||||
import { setupListeners } from '@reduxjs/toolkit/query';
|
||||
import { Middleware } from 'redux';
|
||||
|
||||
import { addReducer, createRootReducer } from '../reducers/root';
|
||||
import { StoreState } from '../types/store';
|
||||
|
||||
import { setStore } from './store';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function addRootReducer(reducers: any) {
|
||||
// this is ok now because we add reducers before configureStore is called
|
||||
// in the future if we want to add reducers during runtime
|
||||
// we'll have to solve this in a more dynamic way
|
||||
addReducer(reducers);
|
||||
}
|
||||
|
||||
const listenerMiddleware = createListenerMiddleware();
|
||||
const extraMiddleware: Middleware[] = [];
|
||||
|
||||
export function addExtraMiddleware(middleware: Middleware) {
|
||||
extraMiddleware.push(middleware);
|
||||
}
|
||||
|
||||
export function configureStore(initialState?: Partial<StoreState>) {
|
||||
const store = reduxConfigureStore({
|
||||
reducer: createRootReducer(),
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({ thunk: true, serializableCheck: false, immutableCheck: false }).concat(
|
||||
listenerMiddleware.middleware,
|
||||
...extraMiddleware
|
||||
),
|
||||
devTools: process.env.NODE_ENV !== 'production',
|
||||
preloadedState: {
|
||||
...initialState,
|
||||
},
|
||||
});
|
||||
|
||||
// this enables "refetchOnFocus" and "refetchOnReconnect" for RTK Query
|
||||
setupListeners(store.dispatch);
|
||||
|
||||
setStore(store);
|
||||
return store;
|
||||
}
|
||||
|
||||
export type RootState = ReturnType<ReturnType<typeof configureStore>['getState']>;
|
||||
export type AppDispatch = ReturnType<typeof configureStore>['dispatch'];
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user