Graphite: Decouple from core Grafana (#110294)
* Add lint rules * Backend decoupling - Add standalone files - Add graphite query type - Add logger to Service - Create logger in the ProvideService method - Use a pointer for the HTTP client provider - Update logger usage everywhere - Update tracer type - Replace simplejson with json - Add dummy CallResource and CheckHealth methods - Update tests * Update ConfigEditor imports * Update types imports * Update datasource - Switch to using semver package - Update imports * Update store imports * Update helper imports and notification creation * Update context import * Update version numbers and logic * Copy array_move from core * Test updates * Add required files and update plugin.json * Update core references and packages * Remove commented code * Update wire * Lint * Fix import * Copy null type * More lint * Update snapshot * Add tests * Review * Fix packages
This commit is contained in:
@@ -101,6 +101,8 @@ linters:
|
||||
- '**/pkg/tsdb/azuremonitor/**/*'
|
||||
- '**/pkg/tsdb/cloud-monitoring/*'
|
||||
- '**/pkg/tsdb/cloud-monitoring/**/*'
|
||||
- '**/pkg/tsdb/graphite/*'
|
||||
- '**/pkg/tsdb/graphite/**/*'
|
||||
- '**/pkg/tsdb/mysql/*'
|
||||
- '**/pkg/tsdb/mysql/**/*'
|
||||
- '**/pkg/tsdb/parca/*'
|
||||
|
||||
@@ -417,6 +417,7 @@ module.exports = [
|
||||
'public/app/plugins/datasource/grafana-postgresql-datasource/**/*.{ts,tsx}',
|
||||
'public/app/plugins/datasource/grafana-pyroscope-datasource/**/*.{ts,tsx}',
|
||||
'public/app/plugins/datasource/grafana-testdata-datasource/**/*.{ts,tsx}',
|
||||
'public/app/plugins/datasource/graphite/**/*.{ts,tsx}',
|
||||
'public/app/plugins/datasource/jaeger/**/*.{ts,tsx}',
|
||||
'public/app/plugins/datasource/loki/**/*.{ts,tsx}',
|
||||
'public/app/plugins/datasource/loki/**/*.{ts,tsx}',
|
||||
|
||||
@@ -383,9 +383,9 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
cloudwatchService := cloudwatch.ProvideService()
|
||||
cloudmonitoringService := cloudmonitoring.ProvideService(httpclientProvider)
|
||||
elasticsearchService := elasticsearch.ProvideService(httpclientProvider)
|
||||
graphiteService := graphite.ProvideService(httpclientProvider, tracingService)
|
||||
influxdbService := influxdb.ProvideService(httpclientProvider, featureToggles)
|
||||
tracer := otelTracer()
|
||||
graphiteService := graphite.ProvideService(httpclientProvider, tracer)
|
||||
influxdbService := influxdb.ProvideService(httpclientProvider, featureToggles)
|
||||
lokiService := loki.ProvideService(httpclientProvider, tracer)
|
||||
opentsdbService := opentsdb.ProvideService(httpclientProvider)
|
||||
prometheusService := prometheus.ProvideService(httpclientProvider)
|
||||
@@ -974,9 +974,9 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
cloudwatchService := cloudwatch.ProvideService()
|
||||
cloudmonitoringService := cloudmonitoring.ProvideService(httpclientProvider)
|
||||
elasticsearchService := elasticsearch.ProvideService(httpclientProvider)
|
||||
graphiteService := graphite.ProvideService(httpclientProvider, tracingService)
|
||||
influxdbService := influxdb.ProvideService(httpclientProvider, featureToggles)
|
||||
tracer := otelTracer()
|
||||
graphiteService := graphite.ProvideService(httpclientProvider, tracer)
|
||||
influxdbService := influxdb.ProvideService(httpclientProvider, featureToggles)
|
||||
lokiService := loki.ProvideService(httpclientProvider, tracer)
|
||||
opentsdbService := opentsdb.ProvideService(httpclientProvider)
|
||||
prometheusService := prometheus.ProvideService(httpclientProvider)
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
"path": "public/plugins/grafana-azure-monitor-datasource/img/azure_monitor_cpu.png"
|
||||
}
|
||||
],
|
||||
"version": "12.1.0-pre",
|
||||
"version": "12.2.0-pre",
|
||||
"updated": "",
|
||||
"keywords": [
|
||||
"azure",
|
||||
@@ -880,7 +880,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.1.0-pre",
|
||||
"version": "12.2.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -934,7 +934,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.1.0-pre",
|
||||
"version": "12.2.0-pre",
|
||||
"updated": "",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -1000,12 +1000,12 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"version": "12.2.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaDependency": "\u003e=10.3.0",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
@@ -1217,7 +1217,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.1.0-pre",
|
||||
"version": "12.2.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -1325,7 +1325,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.1.0-pre",
|
||||
"version": "12.2.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -1375,7 +1375,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.1.0-pre",
|
||||
"version": "12.2.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -1425,7 +1425,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.1.0-pre",
|
||||
"version": "12.2.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -1629,7 +1629,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.1.0-pre",
|
||||
"version": "12.2.0-pre",
|
||||
"updated": "",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -1734,7 +1734,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.1.0-pre",
|
||||
"version": "12.2.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -2042,7 +2042,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.1.0-pre",
|
||||
"version": "12.2.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -2092,7 +2092,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.1.0-pre",
|
||||
"version": "12.2.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -2445,7 +2445,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "12.1.0-pre",
|
||||
"version": "12.2.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
@@ -2467,4 +2467,4 @@
|
||||
"signatureOrg": "",
|
||||
"angularDetected": false
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -16,23 +16,19 @@ import (
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/httpclient"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var logger = log.New("tsdb.graphite")
|
||||
|
||||
type Service struct {
|
||||
im instancemgmt.InstanceManager
|
||||
tracer tracing.Tracer
|
||||
tracer trace.Tracer
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -40,10 +36,12 @@ const (
|
||||
TargetModelField = "target"
|
||||
)
|
||||
|
||||
func ProvideService(httpClientProvider httpclient.Provider, tracer tracing.Tracer) *Service {
|
||||
func ProvideService(httpClientProvider *httpclient.Provider, tracer trace.Tracer) *Service {
|
||||
logger := backend.NewLoggerWith("logger", "graphite")
|
||||
return &Service{
|
||||
im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider)),
|
||||
tracer: tracer,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +51,7 @@ type datasourceInfo struct {
|
||||
Id int64
|
||||
}
|
||||
|
||||
func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc {
|
||||
func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.InstanceFactoryFunc {
|
||||
return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
opts, err := settings.HTTPClientOptions(ctx)
|
||||
if err != nil {
|
||||
@@ -89,8 +87,6 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
|
||||
return nil, fmt.Errorf("query contains no queries")
|
||||
}
|
||||
|
||||
logger := logger.FromContext(ctx)
|
||||
|
||||
// get datasource info from context
|
||||
dsInfo, err := s.getDSInfo(ctx, req.PluginContext)
|
||||
if err != nil {
|
||||
@@ -103,7 +99,7 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
|
||||
formData url.Values
|
||||
}{}
|
||||
for _, query := range req.Queries {
|
||||
graphiteReq, formData, emptyQuery, err := s.createGraphiteRequest(ctx, query, logger, dsInfo)
|
||||
graphiteReq, formData, emptyQuery, err := s.createGraphiteRequest(ctx, query, dsInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -124,7 +120,7 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
|
||||
|
||||
var result = backend.QueryDataResponse{}
|
||||
if len(emptyQueries) != 0 {
|
||||
logger.Warn("Found query models without targets", "models without targets", strings.Join(emptyQueries, "\n"))
|
||||
s.logger.Warn("Found query models without targets", "models without targets", strings.Join(emptyQueries, "\n"))
|
||||
// If no queries had a valid target, return an error; otherwise, attempt with the targets we have
|
||||
if len(emptyQueries) == len(req.Queries) {
|
||||
if result.Responses == nil {
|
||||
@@ -141,7 +137,7 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
|
||||
frames := data.Frames{}
|
||||
|
||||
for refId, graphiteReq := range graphiteQueries {
|
||||
ctx, span := s.tracer.Start(ctx, "graphite query")
|
||||
_, span := s.tracer.Start(ctx, "graphite query")
|
||||
defer span.End()
|
||||
targetStr := strings.Join(graphiteReq.formData["target"], ",")
|
||||
span.SetAttributes(
|
||||
@@ -152,7 +148,6 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
|
||||
attribute.Int64("datasource_id", dsInfo.Id),
|
||||
attribute.Int64("org_id", req.PluginContext.OrgID),
|
||||
)
|
||||
s.tracer.Inject(ctx, graphiteReq.req.Header, span)
|
||||
res, err := dsInfo.HTTPClient.Do(graphiteReq.req)
|
||||
if res != nil {
|
||||
span.SetAttributes(attribute.Int("graphite.response.code", res.StatusCode))
|
||||
@@ -166,11 +161,11 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
|
||||
defer func() {
|
||||
err := res.Body.Close()
|
||||
if err != nil {
|
||||
logger.Warn("Failed to close response body", "error", err)
|
||||
s.logger.Warn("Failed to close response body", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
queryFrames, err := s.toDataFrames(logger, res, refId)
|
||||
queryFrames, err := s.toDataFrames(res, refId)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
@@ -200,28 +195,29 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
|
||||
|
||||
// processQuery converts a Graphite data source query to a Graphite query target. It returns the target,
|
||||
// and the model if the target is invalid
|
||||
func (s *Service) processQuery(logger log.Logger, query backend.DataQuery) (string, *simplejson.Json, error) {
|
||||
model, err := simplejson.NewJson(query.JSON)
|
||||
func (s *Service) processQuery(query backend.DataQuery) (string, *GraphiteQuery, error) {
|
||||
queryJSON := GraphiteQuery{}
|
||||
err := json.Unmarshal(query.JSON, &queryJSON)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
logger.Debug("Graphite", "query", model)
|
||||
currTarget := ""
|
||||
if fullTarget, err := model.Get(TargetFullModelField).String(); err == nil {
|
||||
currTarget = fullTarget
|
||||
} else {
|
||||
currTarget = model.Get(TargetModelField).MustString()
|
||||
return "", &queryJSON, fmt.Errorf("failed to decode the Graphite query: %w", err)
|
||||
}
|
||||
s.logger.Debug("Graphite", "query", queryJSON)
|
||||
currTarget := queryJSON.TargetFull
|
||||
|
||||
if currTarget == "" {
|
||||
logger.Debug("Graphite", "empty query target", model)
|
||||
return "", model, nil
|
||||
currTarget = queryJSON.Target
|
||||
}
|
||||
|
||||
if currTarget == "" {
|
||||
s.logger.Debug("Graphite", "empty query target", queryJSON)
|
||||
return "", &queryJSON, nil
|
||||
}
|
||||
target := fixIntervalFormat(currTarget)
|
||||
|
||||
return target, nil, nil
|
||||
}
|
||||
|
||||
func (s *Service) createRequest(ctx context.Context, l log.Logger, dsInfo *datasourceInfo, data url.Values) (*http.Request, error) {
|
||||
func (s *Service) createRequest(ctx context.Context, dsInfo *datasourceInfo, data url.Values) (*http.Request, error) {
|
||||
u, err := url.Parse(dsInfo.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -230,7 +226,7 @@ func (s *Service) createRequest(ctx context.Context, l log.Logger, dsInfo *datas
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
logger.Info("Failed to create request", "error", err)
|
||||
s.logger.Info("Failed to create request", "error", err)
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
@@ -238,7 +234,7 @@ func (s *Service) createRequest(ctx context.Context, l log.Logger, dsInfo *datas
|
||||
return req, err
|
||||
}
|
||||
|
||||
func (s *Service) createGraphiteRequest(ctx context.Context, query backend.DataQuery, logger log.Logger, dsInfo *datasourceInfo) (*http.Request, url.Values, *simplejson.Json, error) {
|
||||
func (s *Service) createGraphiteRequest(ctx context.Context, query backend.DataQuery, dsInfo *datasourceInfo) (*http.Request, url.Values, *GraphiteQuery, error) {
|
||||
/*
|
||||
graphite doc about from and until, with sdk we are getting absolute instead of relative time
|
||||
https://graphite-api.readthedocs.io/en/latest/api.html#from-until
|
||||
@@ -252,23 +248,21 @@ func (s *Service) createGraphiteRequest(ctx context.Context, query backend.DataQ
|
||||
"target": []string{},
|
||||
}
|
||||
|
||||
target, emptyQuery, err := s.processQuery(logger, query)
|
||||
target, emptyQuery, err := s.processQuery(query)
|
||||
if err != nil {
|
||||
return nil, formData, nil, err
|
||||
}
|
||||
|
||||
if emptyQuery != nil {
|
||||
logger.Debug("Graphite", "empty query target", emptyQuery)
|
||||
s.logger.Debug("Graphite", "empty query target", emptyQuery)
|
||||
return nil, formData, emptyQuery, nil
|
||||
}
|
||||
|
||||
formData["target"] = []string{target}
|
||||
|
||||
if setting.Env == setting.Dev {
|
||||
logger.Debug("Graphite request", "params", formData)
|
||||
}
|
||||
s.logger.Debug("Graphite request", "params", formData)
|
||||
|
||||
graphiteReq, err := s.createRequest(ctx, logger, dsInfo, formData)
|
||||
graphiteReq, err := s.createRequest(ctx, dsInfo, formData)
|
||||
if err != nil {
|
||||
return nil, formData, nil, err
|
||||
}
|
||||
@@ -276,34 +270,34 @@ func (s *Service) createGraphiteRequest(ctx context.Context, query backend.DataQ
|
||||
return graphiteReq, formData, emptyQuery, nil
|
||||
}
|
||||
|
||||
func (s *Service) parseResponse(logger log.Logger, res *http.Response) ([]TargetResponseDTO, error) {
|
||||
func (s *Service) parseResponse(res *http.Response) ([]TargetResponseDTO, error) {
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err := res.Body.Close(); err != nil {
|
||||
logger.Warn("Failed to close response body", "err", err)
|
||||
s.logger.Warn("Failed to close response body", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if res.StatusCode/100 != 2 {
|
||||
logger.Info("Request failed", "status", res.Status, "body", string(body))
|
||||
s.logger.Info("Request failed", "status", res.Status, "body", string(body))
|
||||
return nil, fmt.Errorf("request failed, status: %s", res.Status)
|
||||
}
|
||||
|
||||
var data []TargetResponseDTO
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
logger.Info("Failed to unmarshal graphite response", "error", err, "status", res.Status, "body", string(body))
|
||||
s.logger.Info("Failed to unmarshal graphite response", "error", err, "status", res.Status, "body", string(body))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s *Service) toDataFrames(logger log.Logger, response *http.Response, refId string) (frames data.Frames, error error) {
|
||||
responseData, err := s.parseResponse(logger, response)
|
||||
func (s *Service) toDataFrames(response *http.Response, refId string) (frames data.Frames, error error) {
|
||||
responseData, err := s.parseResponse(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -340,9 +334,7 @@ func (s *Service) toDataFrames(logger log.Logger, response *http.Response, refId
|
||||
data.NewField("value", tags, values).SetConfig(&data.FieldConfig{DisplayNameFromDS: series.Target})).SetMeta(
|
||||
&data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti}))
|
||||
|
||||
if setting.Env == setting.Dev {
|
||||
logger.Debug("Graphite response", "target", series.Target, "datapoints", len(series.DataPoints))
|
||||
}
|
||||
s.logger.Debug("Graphite response", "target", series.Target, "datapoints", len(series.DataPoints))
|
||||
}
|
||||
return frames, nil
|
||||
}
|
||||
@@ -381,3 +373,14 @@ func parseDataTimePoint(dataTimePoint DataTimePoint) (time.Time, *float64, error
|
||||
return timestamp, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
return &backend.CheckHealthResult{
|
||||
Status: backend.HealthStatusOk,
|
||||
Message: "Successfully connected to Graphite.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -12,13 +12,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/httpclient"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"go.opentelemetry.io/otel/trace/noop"
|
||||
)
|
||||
|
||||
func TestFixIntervalFormat(t *testing.T) {
|
||||
@@ -58,8 +56,9 @@ func TestFixIntervalFormat(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProcessQuery(t *testing.T) {
|
||||
service := &Service{}
|
||||
log := logger.FromContext(context.Background())
|
||||
service := &Service{
|
||||
logger: backend.Logger,
|
||||
}
|
||||
t.Run("Parses single valid query", func(t *testing.T) {
|
||||
queries := []backend.DataQuery{
|
||||
{
|
||||
@@ -69,7 +68,7 @@ func TestProcessQuery(t *testing.T) {
|
||||
}`),
|
||||
},
|
||||
}
|
||||
target, jsonModel, err := service.processQuery(log, queries[0])
|
||||
target, jsonModel, err := service.processQuery(queries[0])
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, jsonModel)
|
||||
assert.Equal(t, "app.grafana.*.dashboards.views.1M.count", target)
|
||||
@@ -84,10 +83,10 @@ func TestProcessQuery(t *testing.T) {
|
||||
}`),
|
||||
},
|
||||
}
|
||||
jsonEqual, _ := simplejson.NewJson([]byte(`{"target": ""}`))
|
||||
target, jsonModel, err := service.processQuery(log, queries[0])
|
||||
emptyQuery := GraphiteQuery{Target: ""}
|
||||
target, jsonModel, err := service.processQuery(queries[0])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, jsonEqual, jsonModel)
|
||||
assert.Equal(t, &emptyQuery, jsonModel)
|
||||
assert.Equal(t, "", target)
|
||||
})
|
||||
|
||||
@@ -107,7 +106,7 @@ func TestProcessQuery(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
service := ProvideService(httpclient.NewProvider(), tracing.NewNoopTracerService())
|
||||
service := ProvideService(httpclient.NewProvider(), noop.NewTracerProvider().Tracer("graphite-tests"))
|
||||
|
||||
rsp, err := service.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{
|
||||
@@ -124,7 +123,9 @@ func TestProcessQuery(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("QueryData with no queries returns an error", func(t *testing.T) {
|
||||
service := &Service{}
|
||||
service := &Service{
|
||||
logger: backend.Logger,
|
||||
}
|
||||
|
||||
rsp, err := service.QueryData(context.Background(), &backend.QueryDataRequest{})
|
||||
assert.Nil(t, rsp)
|
||||
@@ -143,7 +144,7 @@ func TestProcessQuery(t *testing.T) {
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
service := ProvideService(httpclient.NewProvider(), tracing.NewNoopTracerService())
|
||||
service := ProvideService(httpclient.NewProvider(), noop.NewTracerProvider().Tracer("graphite-tests"))
|
||||
|
||||
queries := []backend.DataQuery{
|
||||
{
|
||||
@@ -175,7 +176,9 @@ func TestProcessQuery(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConvertResponses(t *testing.T) {
|
||||
service := &Service{}
|
||||
service := &Service{
|
||||
logger: backend.Logger,
|
||||
}
|
||||
|
||||
t.Run("Converts response without tags to data frames", func(*testing.T) {
|
||||
body := `
|
||||
@@ -195,7 +198,7 @@ func TestConvertResponses(t *testing.T) {
|
||||
expectedFrames := data.Frames{expectedFrame}
|
||||
|
||||
httpResponse := &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body))}
|
||||
dataFrames, err := service.toDataFrames(logger, httpResponse, refId)
|
||||
dataFrames, err := service.toDataFrames(httpResponse, refId)
|
||||
|
||||
require.NoError(t, err)
|
||||
if !reflect.DeepEqual(expectedFrames, dataFrames) {
|
||||
@@ -229,7 +232,7 @@ func TestConvertResponses(t *testing.T) {
|
||||
expectedFrames := data.Frames{expectedFrame}
|
||||
|
||||
httpResponse := &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body))}
|
||||
dataFrames, err := service.toDataFrames(logger, httpResponse, refId)
|
||||
dataFrames, err := service.toDataFrames(httpResponse, refId)
|
||||
|
||||
require.NoError(t, err)
|
||||
if !reflect.DeepEqual(expectedFrames, dataFrames) {
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// Copied from Grafana core https://github.com/grafana/grafana/blob/main/pkg/components/null/float.go
|
||||
// Graphite DataPoints utilise the Float type here https://github.com/grafana/grafana/blob/main/pkg/tsdb/graphite/types.go#L10
|
||||
package graphite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
nullString = "null"
|
||||
)
|
||||
|
||||
// Float is a nullable float64.
|
||||
// It does not consider zero values to be null.
|
||||
// It will decode to null, not zero, if null.
|
||||
type Float struct {
|
||||
sql.NullFloat64
|
||||
}
|
||||
|
||||
// NewFloat creates a new Float
|
||||
func NewFloat(f float64, valid bool) Float {
|
||||
return Float{
|
||||
NullFloat64: sql.NullFloat64{
|
||||
Float64: f,
|
||||
Valid: valid,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// FloatFrom creates a new Float that will always be valid.
|
||||
func FloatFrom(f float64) Float {
|
||||
return NewFloat(f, true)
|
||||
}
|
||||
|
||||
// FloatFromPtr creates a new Float that be null if f is nil.
|
||||
func FloatFromPtr(f *float64) Float {
|
||||
if f == nil {
|
||||
return NewFloat(0, false)
|
||||
}
|
||||
return NewFloat(*f, true)
|
||||
}
|
||||
|
||||
// FloatFromString creates a new Float from string f.
|
||||
// If the string is equal to the value of nullString then the Float will be null.
|
||||
// An empty string f will return an error.
|
||||
func FloatFromString(f string, nullString string) (Float, error) {
|
||||
if f == nullString {
|
||||
return FloatFromPtr(nil), nil
|
||||
}
|
||||
fV, err := strconv.ParseFloat(f, 64)
|
||||
if err != nil {
|
||||
return Float{}, err
|
||||
}
|
||||
return FloatFrom(fV), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
// It supports number and null input.
|
||||
// 0 will not be considered a null Float.
|
||||
// It also supports unmarshaling a sql.NullFloat64.
|
||||
func (f *Float) UnmarshalJSON(data []byte) error {
|
||||
var err error
|
||||
var v any
|
||||
if err = json.Unmarshal(data, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case float64:
|
||||
f.Float64 = x
|
||||
case map[string]any:
|
||||
err = json.Unmarshal(data, &f.NullFloat64)
|
||||
case nil:
|
||||
f.Valid = false
|
||||
return nil
|
||||
default:
|
||||
err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Float", reflect.TypeOf(v).Name())
|
||||
}
|
||||
f.Valid = err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||
// It will unmarshal to a null Float if the input is a blank or not an integer.
|
||||
// It will return an error if the input is not an integer, blank, or "null".
|
||||
func (f *Float) UnmarshalText(text []byte) error {
|
||||
str := string(text)
|
||||
if str == "" || str == nullString {
|
||||
f.Valid = false
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
f.Float64, err = strconv.ParseFloat(string(text), 64)
|
||||
f.Valid = err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
// It will encode null if this Float is null, NaN, of Inf.
|
||||
func (f Float) MarshalJSON() ([]byte, error) {
|
||||
if !f.Valid || math.IsNaN(f.Float64) || math.IsInf(f.Float64, 0) {
|
||||
return []byte(nullString), nil
|
||||
}
|
||||
return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler.
|
||||
// It will encode a blank string if this Float is null.
|
||||
func (f Float) MarshalText() ([]byte, error) {
|
||||
if !f.Valid {
|
||||
return []byte{}, nil
|
||||
}
|
||||
return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler.
|
||||
// It will encode a blank string if this Float is null.
|
||||
func (f Float) String() string {
|
||||
if !f.Valid {
|
||||
return nullString
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%1.3f", f.Float64)
|
||||
}
|
||||
|
||||
// FullString returns float as string in full precision
|
||||
func (f Float) FullString() string {
|
||||
if !f.Valid {
|
||||
return nullString
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%f", f.Float64)
|
||||
}
|
||||
|
||||
// IsZero returns true for invalid Floats, for future omitempty support (Go 1.4?)
|
||||
// A non-null Float with a 0 value will not be considered zero.
|
||||
func (f Float) IsZero() bool {
|
||||
return !f.Valid
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package graphite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewFloat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input float64
|
||||
valid bool
|
||||
expected Float
|
||||
}{
|
||||
{
|
||||
name: "valid float",
|
||||
input: 42.5,
|
||||
valid: true,
|
||||
expected: Float{
|
||||
NullFloat64: sql.NullFloat64{Float64: 42.5, Valid: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid float",
|
||||
input: 42.5,
|
||||
valid: false,
|
||||
expected: Float{
|
||||
NullFloat64: sql.NullFloat64{Float64: 42.5, Valid: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "zero",
|
||||
input: 0,
|
||||
valid: true,
|
||||
expected: Float{
|
||||
NullFloat64: sql.NullFloat64{Float64: 0, Valid: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := NewFloat(tt.input, tt.valid)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloatFrom(t *testing.T) {
|
||||
result := FloatFrom(123.456)
|
||||
expected := Float{NullFloat64: sql.NullFloat64{Float64: 123.456, Valid: true}}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestFloatFromPtr(t *testing.T) {
|
||||
f := 42.5
|
||||
result := FloatFromPtr(&f)
|
||||
expected := Float{NullFloat64: sql.NullFloat64{Float64: 42.5, Valid: true}}
|
||||
assert.Equal(t, expected, result)
|
||||
|
||||
result = FloatFromPtr(nil)
|
||||
expected = Float{NullFloat64: sql.NullFloat64{Valid: false}}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestFloatFromString(t *testing.T) {
|
||||
result, err := FloatFromString("123.456", "null")
|
||||
require.NoError(t, err)
|
||||
expected := Float{NullFloat64: sql.NullFloat64{Float64: 123.456, Valid: true}}
|
||||
assert.Equal(t, expected, result)
|
||||
|
||||
result, err = FloatFromString("null", "null")
|
||||
require.NoError(t, err)
|
||||
expected = Float{NullFloat64: sql.NullFloat64{Valid: false}}
|
||||
assert.Equal(t, expected, result)
|
||||
|
||||
_, err = FloatFromString("number", "null")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestFloat_UnmarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
expected Float
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "valid float",
|
||||
input: []byte("123.456"),
|
||||
expected: Float{
|
||||
NullFloat64: sql.NullFloat64{Float64: 123.456, Valid: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "null",
|
||||
input: []byte("null"),
|
||||
expected: Float{
|
||||
NullFloat64: sql.NullFloat64{Valid: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sql.NullFloat64 - valid",
|
||||
input: []byte(`{"Float64": 42.5, "Valid": true}`),
|
||||
expected: Float{
|
||||
NullFloat64: sql.NullFloat64{Float64: 42.5, Valid: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sql.NullFloat64 - invalid",
|
||||
input: []byte(`{"Float64": 0, "Valid": false}`),
|
||||
expected: Float{
|
||||
NullFloat64: sql.NullFloat64{Float64: 0, Valid: true}, // Valid gets overridden to true when unmarshaling succeeds
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid JSON",
|
||||
input: []byte("invalid"),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "string input",
|
||||
input: []byte(`"123.456"`),
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var result Float
|
||||
err := result.UnmarshalJSON(tt.input)
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloat_UnmarshalText(t *testing.T) {
|
||||
var f Float
|
||||
err := f.UnmarshalText([]byte("123.456"))
|
||||
require.NoError(t, err)
|
||||
expected := Float{NullFloat64: sql.NullFloat64{Float64: 123.456, Valid: true}}
|
||||
assert.Equal(t, expected, f)
|
||||
|
||||
var f2 Float
|
||||
err = f2.UnmarshalText([]byte(""))
|
||||
require.NoError(t, err)
|
||||
expected = Float{NullFloat64: sql.NullFloat64{Valid: false}}
|
||||
assert.Equal(t, expected, f2)
|
||||
|
||||
var f3 Float
|
||||
err = f3.UnmarshalText([]byte("null"))
|
||||
require.NoError(t, err)
|
||||
expected = Float{NullFloat64: sql.NullFloat64{Valid: false}}
|
||||
assert.Equal(t, expected, f3)
|
||||
|
||||
var f4 Float
|
||||
err = f4.UnmarshalText([]byte("not-a-number"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestFloat_MarshalJSON(t *testing.T) {
|
||||
f := Float{NullFloat64: sql.NullFloat64{Float64: 123.456, Valid: true}}
|
||||
data, err := f.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "123.456", string(data))
|
||||
|
||||
f = Float{NullFloat64: sql.NullFloat64{Valid: false}}
|
||||
data, err = f.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "null", string(data))
|
||||
}
|
||||
|
||||
func TestFloat_MarshalText(t *testing.T) {
|
||||
f := Float{NullFloat64: sql.NullFloat64{Float64: 123.456, Valid: true}}
|
||||
data, err := f.MarshalText()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "123.456", string(data))
|
||||
|
||||
f = Float{NullFloat64: sql.NullFloat64{Valid: false}}
|
||||
data, err = f.MarshalText()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", string(data))
|
||||
}
|
||||
|
||||
func TestFloat_String(t *testing.T) {
|
||||
f := Float{NullFloat64: sql.NullFloat64{Float64: 123.456789, Valid: true}}
|
||||
assert.Equal(t, "123.457", f.String())
|
||||
|
||||
f = Float{NullFloat64: sql.NullFloat64{Valid: false}}
|
||||
assert.Equal(t, "null", f.String())
|
||||
}
|
||||
|
||||
func TestFloat_FullString(t *testing.T) {
|
||||
f := Float{NullFloat64: sql.NullFloat64{Float64: 123.456789, Valid: true}}
|
||||
assert.Equal(t, "123.456789", f.FullString())
|
||||
|
||||
f = Float{NullFloat64: sql.NullFloat64{Valid: false}}
|
||||
assert.Equal(t, "null", f.FullString())
|
||||
}
|
||||
|
||||
func TestFloat_IsZero(t *testing.T) {
|
||||
f := Float{NullFloat64: sql.NullFloat64{Float64: 0, Valid: true}}
|
||||
assert.False(t, f.IsZero())
|
||||
|
||||
f = Float{NullFloat64: sql.NullFloat64{Valid: false}}
|
||||
assert.True(t, f.IsZero())
|
||||
}
|
||||
|
||||
func TestFloat_JSONRoundTrip(t *testing.T) {
|
||||
testCases := []Float{
|
||||
{NullFloat64: sql.NullFloat64{Float64: 123.456, Valid: true}},
|
||||
{NullFloat64: sql.NullFloat64{Float64: 0, Valid: true}},
|
||||
{NullFloat64: sql.NullFloat64{Valid: false}},
|
||||
}
|
||||
|
||||
for _, original := range testCases {
|
||||
// Marshal to JSON
|
||||
data, err := json.Marshal(original)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Unmarshal back
|
||||
var result Float
|
||||
err = json.Unmarshal(data, &result)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Compare
|
||||
assert.Equal(t, original, result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
|
||||
graphite "github.com/grafana/grafana/pkg/tsdb/graphite"
|
||||
)
|
||||
|
||||
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: graphite.ProvideService(httpclient.NewProvider(), tracing.DefaultTracer()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Datasource struct {
|
||||
Service *graphite.Service
|
||||
}
|
||||
|
||||
func contextualMiddlewares(ctx context.Context) context.Context {
|
||||
cfg := backend.GrafanaConfigFromContext(ctx)
|
||||
return httpclient.WithContextualMiddleware(ctx, httpclient.ResponseLimitMiddleware(cfg.ResponseLimit()))
|
||||
}
|
||||
|
||||
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("graphite", NewDatasource, datasource.ManageOpts{}); err != nil {
|
||||
log.DefaultLogger.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
package graphite
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
)
|
||||
|
||||
type TargetResponseDTO struct {
|
||||
Target string `json:"target"`
|
||||
DataPoints DataTimeSeriesPoints `json:"datapoints"`
|
||||
@@ -11,5 +7,14 @@ type TargetResponseDTO struct {
|
||||
Tags map[string]any `json:"tags"`
|
||||
}
|
||||
|
||||
type DataTimePoint [2]null.Float
|
||||
type DataTimePoint [2]Float
|
||||
type DataTimeSeriesPoints []DataTimePoint
|
||||
|
||||
type GraphiteQuery struct {
|
||||
QueryType string `json:"queryType"`
|
||||
TextEditor *bool `json:"textEditor,omitempty"`
|
||||
Target string `json:"target,omitempty"`
|
||||
TargetFull string `json:"targetFull,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
FromAnnotations *bool `json:"fromAnnotations,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
const graphitePlugin = async () =>
|
||||
await import(/* webpackChunkName: "graphitePlugin" */ 'app/plugins/datasource/graphite/module');
|
||||
const cloudwatchPlugin = async () =>
|
||||
await import(/* webpackChunkName: "cloudwatchPlugin" */ 'app/plugins/datasource/cloudwatch/module');
|
||||
const dashboardDSPlugin = async () =>
|
||||
@@ -72,7 +70,6 @@ const nodeGraph = async () =>
|
||||
|
||||
const builtInPlugins: Record<string, System.Module | (() => Promise<System.Module>)> = {
|
||||
// datasources
|
||||
'core:plugin/graphite': graphitePlugin,
|
||||
'core:plugin/cloudwatch': cloudwatchPlugin,
|
||||
'core:plugin/dashboard': dashboardDSPlugin,
|
||||
'core:plugin/elasticsearch': elasticsearchPlugin,
|
||||
|
||||
@@ -2,8 +2,8 @@ import { css, cx } from '@emotion/css';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { MetadataInspectorProps, rangeUtil } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { stylesFactory } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
import { GraphiteDatasource } from '../datasource';
|
||||
import { getRollupNotice, getRuntimeConsolidationNotice, parseSchemaRetentions } from '../meta';
|
||||
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
updateDatasourcePluginJsonDataOption,
|
||||
onUpdateDatasourceJsonDataOptionSelect,
|
||||
onUpdateDatasourceJsonDataOptionChecked,
|
||||
store,
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Alert, DataSourceHttpSettings, Field, FieldSet, Select, Switch } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
import store from 'app/core/store';
|
||||
|
||||
import { GraphiteOptions, GraphiteType } from '../types';
|
||||
import { DEFAULT_GRAPHITE_VERSION, GRAPHITE_VERSIONS } from '../versions';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { isArray } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { of } from 'rxjs';
|
||||
import { createFetchResponse } from 'test/helpers/createFetchResponse';
|
||||
|
||||
import {
|
||||
AbstractLabelMatcher,
|
||||
@@ -11,19 +10,27 @@ import {
|
||||
dateTime,
|
||||
getFrameDisplayName,
|
||||
MetricFindValue,
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
import { BackendSrvRequest } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { BackendSrvRequest, FetchResponse, getTemplateSrv, TemplateSrv, VariableInterpolation } from '@grafana/runtime';
|
||||
|
||||
import { fromString } from './configuration/parseLokiLabelMappings';
|
||||
import { GraphiteDatasource } from './datasource';
|
||||
import { GraphiteQuery, GraphiteQueryType } from './types';
|
||||
import { DEFAULT_GRAPHITE_VERSION } from './versions';
|
||||
|
||||
const fetchMock = jest.fn();
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getBackendSrv: () => backendSrv,
|
||||
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||
getBackendSrv: () => ({
|
||||
fetch: fetchMock,
|
||||
}),
|
||||
getTemplateSrv: () => {
|
||||
return {
|
||||
replace: (s: string) => s,
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
interface Context {
|
||||
@@ -31,9 +38,19 @@ interface Context {
|
||||
ds: GraphiteDatasource;
|
||||
}
|
||||
|
||||
describe('graphiteDatasource', () => {
|
||||
const fetchMock = jest.spyOn(backendSrv, 'fetch');
|
||||
const createFetchResponse = <T>(data: T): FetchResponse<T> => ({
|
||||
data,
|
||||
status: 200,
|
||||
url: 'http://localhost:3000/api/query',
|
||||
config: { url: 'http://localhost:3000/api/query' },
|
||||
type: 'basic',
|
||||
statusText: 'Ok',
|
||||
redirected: false,
|
||||
headers: new Headers(),
|
||||
ok: true,
|
||||
});
|
||||
|
||||
describe('graphiteDatasource', () => {
|
||||
let ctx = {} as Context;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -46,8 +63,9 @@ describe('graphiteDatasource', () => {
|
||||
rollupIndicatorEnabled: true,
|
||||
},
|
||||
};
|
||||
const templateSrv = new TemplateSrv();
|
||||
const templateSrv = getTemplateSrv();
|
||||
const ds = new GraphiteDatasource(instanceSettings, templateSrv);
|
||||
|
||||
ctx = { templateSrv, ds };
|
||||
});
|
||||
|
||||
@@ -449,7 +467,22 @@ describe('graphiteDatasource', () => {
|
||||
});
|
||||
|
||||
it('should replace target placeholder when nesting query references with template variables', () => {
|
||||
ctx.templateSrv.init([{ type: 'query', name: 'metric', current: { value: ['aMetricName'] } }]);
|
||||
const originalReplace = ctx.templateSrv.replace;
|
||||
ctx.templateSrv.replace = jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(
|
||||
target?: string | undefined,
|
||||
scopedVars?: ScopedVars | undefined,
|
||||
format?: string | Function,
|
||||
interpolations?: VariableInterpolation[]
|
||||
): string => {
|
||||
if (target?.includes('[[metric]]')) {
|
||||
return target.replaceAll('[[metric]]', 'aMetricName');
|
||||
}
|
||||
return originalReplace(target, scopedVars, format, interpolations);
|
||||
}
|
||||
);
|
||||
const originalTargetMap = {
|
||||
A: '[[metric]]',
|
||||
B: 'sumSeries(#A)',
|
||||
@@ -465,7 +498,22 @@ describe('graphiteDatasource', () => {
|
||||
});
|
||||
|
||||
it('should use scoped variables when nesting query references', () => {
|
||||
ctx.templateSrv.init([{ type: 'query', name: 'metric', current: { value: ['globalValue'] } }]);
|
||||
const originalReplace = ctx.templateSrv.replace;
|
||||
ctx.templateSrv.replace = jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(
|
||||
target?: string | undefined,
|
||||
scopedVars?: ScopedVars | undefined,
|
||||
format?: string | Function,
|
||||
interpolations?: VariableInterpolation[]
|
||||
): string => {
|
||||
if (target?.includes('$metric')) {
|
||||
return target.replaceAll('$metric', 'scopedValue');
|
||||
}
|
||||
return originalReplace(target, scopedVars, format, interpolations);
|
||||
}
|
||||
);
|
||||
|
||||
const originalTargetMap = {
|
||||
A: '$metric',
|
||||
@@ -488,7 +536,22 @@ describe('graphiteDatasource', () => {
|
||||
});
|
||||
|
||||
it('should apply scoped variables to nested references with hidden targets', () => {
|
||||
ctx.templateSrv.init([{ type: 'query', name: 'server', current: { value: ['global'] } }]);
|
||||
const originalReplace = ctx.templateSrv.replace;
|
||||
ctx.templateSrv.replace = jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(
|
||||
target?: string | undefined,
|
||||
scopedVars?: ScopedVars | undefined,
|
||||
format?: string | Function,
|
||||
interpolations?: VariableInterpolation[]
|
||||
): string => {
|
||||
if (target?.includes('$server')) {
|
||||
return target.replaceAll('$server', scopedVars?.server?.value);
|
||||
}
|
||||
return originalReplace(target, scopedVars, format, interpolations);
|
||||
}
|
||||
);
|
||||
|
||||
const originalTargetMap = {
|
||||
A: '$server.cpu',
|
||||
@@ -594,13 +657,22 @@ describe('graphiteDatasource', () => {
|
||||
|
||||
describe('when formatting targets', () => {
|
||||
it('does not attempt to glob for one variable', () => {
|
||||
ctx.templateSrv.init([
|
||||
{
|
||||
type: 'query',
|
||||
name: 'metric',
|
||||
current: { value: ['b'] },
|
||||
},
|
||||
]);
|
||||
const originalReplace = ctx.templateSrv.replace;
|
||||
ctx.templateSrv.replace = jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(
|
||||
target?: string | undefined,
|
||||
scopedVars?: ScopedVars | undefined,
|
||||
format?: string | Function,
|
||||
interpolations?: VariableInterpolation[]
|
||||
): string => {
|
||||
if (target?.includes('$metric')) {
|
||||
return target.replaceAll('$metric', 'b');
|
||||
}
|
||||
return originalReplace(target, scopedVars, format, interpolations);
|
||||
}
|
||||
);
|
||||
|
||||
const originalTargetMap = {
|
||||
A: 'my.$metric.*',
|
||||
@@ -615,13 +687,23 @@ describe('graphiteDatasource', () => {
|
||||
});
|
||||
|
||||
it('globs for more than one variable', () => {
|
||||
ctx.templateSrv.init([
|
||||
{
|
||||
type: 'query',
|
||||
name: 'metric',
|
||||
current: { value: ['a', 'b'] },
|
||||
},
|
||||
]);
|
||||
const originalReplace = ctx.templateSrv.replace;
|
||||
ctx.templateSrv.replace = jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(
|
||||
target?: string | undefined,
|
||||
scopedVars?: ScopedVars | undefined,
|
||||
format?: string | Function,
|
||||
interpolations?: VariableInterpolation[]
|
||||
): string => {
|
||||
if (target?.includes('[[metric]]')) {
|
||||
return target.replaceAll('[[metric]]', '{a,b}');
|
||||
}
|
||||
return originalReplace(target, scopedVars, format, interpolations);
|
||||
}
|
||||
);
|
||||
|
||||
const originalTargetMap = { A: 'my.[[metric]].*' };
|
||||
const results = ctx.ds.buildGraphiteParams(
|
||||
{
|
||||
@@ -721,13 +803,22 @@ describe('graphiteDatasource', () => {
|
||||
});
|
||||
|
||||
it('/metrics/find should be POST', () => {
|
||||
ctx.templateSrv.init([
|
||||
{
|
||||
type: 'query',
|
||||
name: 'foo',
|
||||
current: { value: ['bar'] },
|
||||
},
|
||||
]);
|
||||
const originalReplace = ctx.templateSrv.replace;
|
||||
ctx.templateSrv.replace = jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(
|
||||
target?: string | undefined,
|
||||
scopedVars?: ScopedVars | undefined,
|
||||
format?: string | Function,
|
||||
interpolations?: VariableInterpolation[]
|
||||
): string => {
|
||||
if (target?.includes('[[foo]]')) {
|
||||
return target.replaceAll('[[foo]]', 'bar');
|
||||
}
|
||||
return originalReplace(target, scopedVars, format, interpolations);
|
||||
}
|
||||
);
|
||||
ctx.ds.metricFindQuery('[[foo]]').then((data) => {
|
||||
results = data;
|
||||
});
|
||||
@@ -739,6 +830,22 @@ describe('graphiteDatasource', () => {
|
||||
});
|
||||
|
||||
it('should interpolate $__searchFilter with searchFilter', () => {
|
||||
const originalReplace = ctx.templateSrv.replace;
|
||||
ctx.templateSrv.replace = jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(
|
||||
target?: string | undefined,
|
||||
scopedVars?: ScopedVars | undefined,
|
||||
format?: string | Function,
|
||||
interpolations?: VariableInterpolation[]
|
||||
): string => {
|
||||
if (target?.includes('$__searchFilter')) {
|
||||
return target.replaceAll('$__searchFilter', 'backend*');
|
||||
}
|
||||
return originalReplace(target, scopedVars, format, interpolations);
|
||||
}
|
||||
);
|
||||
ctx.ds.metricFindQuery('app.$__searchFilter', { searchFilter: 'backend' }).then((data) => {
|
||||
results = data;
|
||||
});
|
||||
@@ -750,6 +857,22 @@ describe('graphiteDatasource', () => {
|
||||
});
|
||||
|
||||
it('should interpolate $__searchFilter with default when searchFilter is missing', () => {
|
||||
const originalReplace = ctx.templateSrv.replace;
|
||||
ctx.templateSrv.replace = jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(
|
||||
target?: string | undefined,
|
||||
scopedVars?: ScopedVars | undefined,
|
||||
format?: string | Function,
|
||||
interpolations?: VariableInterpolation[]
|
||||
): string => {
|
||||
if (target?.includes('$__searchFilter')) {
|
||||
return target.replaceAll('$__searchFilter', '*');
|
||||
}
|
||||
return originalReplace(target, scopedVars, format, interpolations);
|
||||
}
|
||||
);
|
||||
ctx.ds.metricFindQuery('app.$__searchFilter', {}).then((data) => {
|
||||
results = data;
|
||||
});
|
||||
@@ -988,7 +1111,7 @@ describe('graphiteDatasource', () => {
|
||||
function accessScenario(name: string, url: string, fn: ({ headers }: { headers: Record<string, unknown> }) => void) {
|
||||
describe('access scenario ' + name, () => {
|
||||
const ctx = {
|
||||
templateSrv: new TemplateSrv(),
|
||||
templateSrv: getTemplateSrv(),
|
||||
instanceSettings: { url: 'url', name: 'graphiteProd', jsonData: {} },
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { map as _map, each, indexOf, isArray, isString } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { lastValueFrom, merge, Observable, of, OperatorFunction, pipe, throwError } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { coerce, gte, SemVer, valid } from 'semver';
|
||||
|
||||
import {
|
||||
AbstractLabelMatcher,
|
||||
@@ -22,16 +23,14 @@ import {
|
||||
TimeRange,
|
||||
toDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { BackendSrvRequest, FetchResponse, getBackendSrv } from '@grafana/runtime';
|
||||
import { BackendSrvRequest, FetchResponse, getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
|
||||
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { getRollupNotice, getRuntimeConsolidationNotice } from 'app/plugins/datasource/graphite/meta';
|
||||
|
||||
import { AnnotationEditor } from './components/AnnotationsEditor';
|
||||
import { convertToGraphiteQueryObject } from './components/helpers';
|
||||
import gfunc, { FuncDef, FuncDefs, FuncInstance } from './gfunc';
|
||||
import GraphiteQueryModel from './graphite_query';
|
||||
import { getRollupNotice, getRuntimeConsolidationNotice } from './meta';
|
||||
import { prepareAnnotation } from './migrations';
|
||||
// Types
|
||||
import {
|
||||
@@ -889,8 +888,8 @@ export class GraphiteDatasource
|
||||
this.doGraphiteRequest(httpOptions).pipe(
|
||||
map((results: FetchResponse) => {
|
||||
if (results.data) {
|
||||
const semver = new SemVersion(results.data);
|
||||
return semver.isValid() ? results.data : '';
|
||||
const semver = new SemVer(results.data);
|
||||
return valid(semver) ? results.data : '';
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
@@ -1079,11 +1078,19 @@ export class GraphiteDatasource
|
||||
}
|
||||
|
||||
function supportsTags(version: string): boolean {
|
||||
return isVersionGtOrEq(version, '1.1');
|
||||
const fullVersion = coerce(version);
|
||||
if (!fullVersion) {
|
||||
return false;
|
||||
}
|
||||
return gte(fullVersion, '1.1.0');
|
||||
}
|
||||
|
||||
function supportsFunctionIndex(version: string): boolean {
|
||||
return isVersionGtOrEq(version, '1.1');
|
||||
const fullVersion = coerce(version);
|
||||
if (!fullVersion) {
|
||||
return false;
|
||||
}
|
||||
return gte(fullVersion, '1.1.0');
|
||||
}
|
||||
|
||||
function mapToTags(): OperatorFunction<FetchResponse, Array<{ text: string }>> {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { EventBusExtended } from '@grafana/data';
|
||||
import { setBackendSrv } from '@grafana/runtime';
|
||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
import { ContextSrv, User } from '../../../core/services/context_srv';
|
||||
import { BackendSrv, getBackendSrv, setBackendSrv } from '@grafana/runtime';
|
||||
|
||||
import { GraphiteDatasource } from './datasource';
|
||||
|
||||
@@ -12,12 +8,13 @@ interface Context {
|
||||
ds: GraphiteDatasource;
|
||||
}
|
||||
|
||||
let origBackendSrv: BackendSrv;
|
||||
describe('graphiteDatasource integration with backendSrv and fetch', () => {
|
||||
let ctx = {} as Context;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
origBackendSrv = getBackendSrv();
|
||||
const instanceSettings = {
|
||||
url: '/api/datasources/proxy/1',
|
||||
name: 'graphiteProd',
|
||||
@@ -29,12 +26,15 @@ describe('graphiteDatasource integration with backendSrv and fetch', () => {
|
||||
ctx = { ds };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setBackendSrv(origBackendSrv);
|
||||
});
|
||||
|
||||
describe('returns a list of functions', () => {
|
||||
it('should return a list of functions with invalid JSON', async () => {
|
||||
const INVALID_JSON =
|
||||
'{"testFunction":{"name":"function","description":"description","module":"graphite.render.functions","group":"Transform","params":[{"name":"param","type":"intOrInf","required":true,"default":Infinity}]}}';
|
||||
|
||||
mockBackendSrv(INVALID_JSON);
|
||||
setBackendSrv({ ...origBackendSrv, fetch: jest.fn().mockReturnValue(of({ data: INVALID_JSON })) });
|
||||
|
||||
const funcDefs = await ctx.ds.getFuncDefs();
|
||||
|
||||
@@ -61,8 +61,7 @@ describe('graphiteDatasource integration with backendSrv and fetch', () => {
|
||||
it('should return a list of functions with valid JSON', async () => {
|
||||
const VALID_JSON =
|
||||
'{"testFunction":{"name":"function","description":"description","module":"graphite.render.functions","group":"Transform","params":[{"name":"param","type":"intOrInf","required":true,"default":1e9999}]}}';
|
||||
|
||||
mockBackendSrv(VALID_JSON);
|
||||
setBackendSrv({ ...origBackendSrv, fetch: jest.fn().mockReturnValue(of({ data: VALID_JSON })) });
|
||||
|
||||
const funcDefs = await ctx.ds.getFuncDefs();
|
||||
|
||||
@@ -87,62 +86,3 @@ describe('graphiteDatasource integration with backendSrv and fetch', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockBackendSrv(data: string) {
|
||||
const defaults = {
|
||||
data: '',
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'Ok',
|
||||
isSignedIn: true,
|
||||
orgId: 1337,
|
||||
redirected: false,
|
||||
type: 'basic',
|
||||
url: 'http://localhost:3000/api/some-mock',
|
||||
};
|
||||
|
||||
const props = { ...defaults };
|
||||
|
||||
props.data = data;
|
||||
|
||||
const textMock = jest.fn().mockResolvedValue(props.data);
|
||||
|
||||
const fromFetchMock = jest.fn().mockImplementation(() => {
|
||||
const mockedResponse = {
|
||||
ok: props.ok,
|
||||
status: props.status,
|
||||
statusText: props.statusText,
|
||||
text: textMock,
|
||||
redirected: false,
|
||||
type: 'basic',
|
||||
url: 'http://localhost:3000/api/some-mock',
|
||||
headers: new Headers({
|
||||
method: 'GET',
|
||||
url: '/functions',
|
||||
// to work around Graphite returning invalid JSON
|
||||
responseType: 'text',
|
||||
}),
|
||||
};
|
||||
return of(mockedResponse);
|
||||
});
|
||||
|
||||
const appEventsMock = {} as EventBusExtended;
|
||||
|
||||
const user: User = {
|
||||
isSignedIn: props.isSignedIn,
|
||||
orgId: props.orgId,
|
||||
} as unknown as User;
|
||||
const contextSrvMock: ContextSrv = {
|
||||
user,
|
||||
} as unknown as ContextSrv;
|
||||
const logoutMock = jest.fn();
|
||||
|
||||
const mockedBackendSrv = new BackendSrv({
|
||||
fromFetch: fromFetchMock,
|
||||
appEvents: appEventsMock,
|
||||
contextSrv: contextSrvMock,
|
||||
logout: logoutMock,
|
||||
});
|
||||
|
||||
setBackendSrv(mockedBackendSrv);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { assign, each, filter, forEach, get, includes, isString, last, map, toString, isFinite } from 'lodash';
|
||||
import { assign, each, filter, forEach, get, includes, isFinite, isString, last, map, toString } from 'lodash';
|
||||
import { coerce, gte } from 'semver';
|
||||
|
||||
import { InterpolateFunction } from '@grafana/data';
|
||||
import { isVersionGtOrEq } from 'app/core/utils/version';
|
||||
|
||||
export type ParamDef = {
|
||||
name: string;
|
||||
@@ -665,7 +665,7 @@ addFuncDef({
|
||||
},
|
||||
],
|
||||
defaultParams: ['avg'],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -673,7 +673,7 @@ addFuncDef({
|
||||
category: 'Filter Series',
|
||||
params: [{ name: 'n', type: 'int' }],
|
||||
defaultParams: [95],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -681,7 +681,7 @@ addFuncDef({
|
||||
category: 'Transform',
|
||||
params: [{ name: 'steps', type: 'int' }],
|
||||
defaultParams: [1],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -695,7 +695,7 @@ addFuncDef({
|
||||
},
|
||||
],
|
||||
defaultParams: [10],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -703,7 +703,7 @@ addFuncDef({
|
||||
category: 'Special',
|
||||
params: [{ name: 'fallback', type: 'string' }],
|
||||
defaultParams: ['constantLine(0)'],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -711,7 +711,7 @@ addFuncDef({
|
||||
category: 'Filter Series',
|
||||
params: [{ name: 'grep', type: 'string' }],
|
||||
defaultParams: ['grep'],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -731,7 +731,7 @@ addFuncDef({
|
||||
},
|
||||
],
|
||||
defaultParams: ['sum', 3],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -745,7 +745,7 @@ addFuncDef({
|
||||
},
|
||||
],
|
||||
defaultParams: ['1d'],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -753,19 +753,19 @@ addFuncDef({
|
||||
category: 'Transform',
|
||||
params: [{ name: 'limit', type: 'int', optional: true }],
|
||||
defaultParams: [],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
name: 'invert',
|
||||
category: 'Transform',
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
name: 'isNonNull',
|
||||
category: 'Combine',
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -786,7 +786,7 @@ addFuncDef({
|
||||
},
|
||||
],
|
||||
defaultParams: [],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -795,7 +795,7 @@ addFuncDef({
|
||||
params: [{ name: 'node', type: 'int' }],
|
||||
defaultParams: [3],
|
||||
category: 'Combine',
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -809,7 +809,7 @@ addFuncDef({
|
||||
},
|
||||
],
|
||||
defaultParams: [10],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -823,7 +823,7 @@ addFuncDef({
|
||||
},
|
||||
],
|
||||
defaultParams: [10],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -837,7 +837,7 @@ addFuncDef({
|
||||
},
|
||||
],
|
||||
defaultParams: [10],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -852,13 +852,13 @@ addFuncDef({
|
||||
},
|
||||
],
|
||||
defaultParams: [2],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
name: 'offsetToZero',
|
||||
category: 'Transform',
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -866,7 +866,7 @@ addFuncDef({
|
||||
category: 'Transform',
|
||||
params: [{ name: 'factor', type: 'int' }],
|
||||
defaultParams: [10],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -874,7 +874,7 @@ addFuncDef({
|
||||
category: 'Transform',
|
||||
params: optionalSeriesRefArgs,
|
||||
defaultParams: [''],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -895,7 +895,7 @@ addFuncDef({
|
||||
],
|
||||
defaultParams: ['asPercent', 2, 'used_bytes'],
|
||||
category: 'Combine',
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -903,19 +903,19 @@ addFuncDef({
|
||||
category: 'Filter Series',
|
||||
params: [{ name: 'n', type: 'int' }],
|
||||
defaultParams: [95],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
name: 'removeEmptySeries',
|
||||
category: 'Filter Series',
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
name: 'squareRoot',
|
||||
category: 'Transform',
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -935,7 +935,7 @@ addFuncDef({
|
||||
},
|
||||
],
|
||||
defaultParams: ['-1h'],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -950,14 +950,14 @@ addFuncDef({
|
||||
},
|
||||
],
|
||||
defaultParams: ['#A', 4],
|
||||
version: '1.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
name: 'seriesByTag',
|
||||
category: 'Special',
|
||||
params: [{ name: 'tagExpression', type: 'string', multiple: true }],
|
||||
version: '1.1',
|
||||
version: '1.1.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -972,7 +972,7 @@ addFuncDef({
|
||||
{ name: 'tag', type: 'string', multiple: true },
|
||||
],
|
||||
defaultParams: ['sum', 'tag'],
|
||||
version: '1.1',
|
||||
version: '1.1.0',
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -980,11 +980,16 @@ addFuncDef({
|
||||
category: 'Alias',
|
||||
params: [{ name: 'tag', type: 'string', multiple: true }],
|
||||
defaultParams: ['tag'],
|
||||
version: '1.1',
|
||||
version: '1.1.0',
|
||||
});
|
||||
|
||||
function isVersionRelatedFunction(obj: { version?: string }, graphiteVersion: string) {
|
||||
return !obj.version || isVersionGtOrEq(graphiteVersion, obj.version);
|
||||
const fullVersion = coerce(graphiteVersion);
|
||||
if (!fullVersion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !obj.version || gte(fullVersion, obj.version);
|
||||
}
|
||||
|
||||
export class FuncInstance {
|
||||
|
||||
@@ -2,12 +2,12 @@ import { compact, each, findIndex, flatten, get, join, keyBy, last, map, reduce,
|
||||
|
||||
import { ScopedVars } from '@grafana/data';
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
import { arrayMove } from 'app/core/utils/arrayMove';
|
||||
|
||||
import { GraphiteDatasource } from './datasource';
|
||||
import { FuncInstance } from './gfunc';
|
||||
import { AstNode, Parser } from './parser';
|
||||
import { GraphiteSegment } from './types';
|
||||
import { arrayMove } from './utils';
|
||||
|
||||
export type GraphiteTagOperator = '=' | '=~' | '!=' | '!=~';
|
||||
|
||||
|
||||
@@ -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,56 @@
|
||||
{
|
||||
"name": "@grafana-plugins/graphite",
|
||||
"description": "Graphite data source plugin for Grafana",
|
||||
"private": true,
|
||||
"version": "12.2.0-pre",
|
||||
"dependencies": {
|
||||
"@emotion/css": "11.13.5",
|
||||
"@grafana/data": "12.2.0-pre",
|
||||
"@grafana/plugin-ui": "^0.10.10",
|
||||
"@grafana/runtime": "12.2.0-pre",
|
||||
"@grafana/schema": "12.2.0-pre",
|
||||
"@grafana/ui": "12.2.0-pre",
|
||||
"@reduxjs/toolkit": "2.8.2",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.30.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-use": "17.6.0",
|
||||
"redux": "5.0.1",
|
||||
"rxjs": "7.8.2",
|
||||
"semver": "7.7.2",
|
||||
"tslib": "2.8.1",
|
||||
"uuid": "11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grafana/e2e-selectors": "12.2.0-pre",
|
||||
"@grafana/plugin-configs": "12.2.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/node": "22.17.0",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/semver": "7.7.0",
|
||||
"@types/uuid": "10.0.0",
|
||||
"jest": "29.7.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.2",
|
||||
"webpack": "5.101.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@grafana/runtime": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production",
|
||||
"build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)",
|
||||
"dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development",
|
||||
"test": "jest --watch --onlyChanged",
|
||||
"test:ci": "jest --maxWorkers 4"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"type": "module"
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"maxDataPoints": true,
|
||||
"cacheTimeout": true
|
||||
},
|
||||
|
||||
"executable": "gpx_graphite",
|
||||
"info": {
|
||||
"description": "Open source time series database",
|
||||
"author": {
|
||||
@@ -37,6 +37,11 @@
|
||||
},
|
||||
{ "name": "Raise issue", "url": "https://github.com/grafana/grafana/issues/new" },
|
||||
{ "name": "Documentation", "url": "https://grafana.com/docs/grafana/latest/datasources/graphite/" }
|
||||
]
|
||||
],
|
||||
"version": "%VERSION%"
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": ">=10.3.0",
|
||||
"plugins": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "../../../../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:plugin", "type:datasource"],
|
||||
"targets": {
|
||||
"build": {},
|
||||
"dev": {}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TemplateSrvStub } from 'test/specs/helpers';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
|
||||
import gfunc from '../gfunc';
|
||||
import GraphiteQuery from '../graphite_query';
|
||||
@@ -12,7 +12,7 @@ describe('Graphite query model', () => {
|
||||
createFuncInstance: gfunc.createFuncInstance,
|
||||
},
|
||||
// @ts-ignore
|
||||
templateSrv: new TemplateSrvStub(),
|
||||
templateSrv: getTemplateSrv(),
|
||||
targets: [],
|
||||
};
|
||||
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { TemplateSrvStub } from 'test/specs/helpers';
|
||||
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
|
||||
import { GraphiteDatasource } from '../datasource';
|
||||
import gfunc from '../gfunc';
|
||||
import { actions } from '../state/actions';
|
||||
import {
|
||||
getAltSegmentsSelectables,
|
||||
getTagsSelectables,
|
||||
getTagsAsSegmentsSelectables,
|
||||
getTagsSelectables,
|
||||
getTagValuesSelectables,
|
||||
} from '../state/providers';
|
||||
import { createStore } from '../state/store';
|
||||
import { GraphiteSegment } from '../types';
|
||||
|
||||
jest.mock('app/store/store', () => ({
|
||||
dispatch: jest.fn(),
|
||||
const mockPublish = jest.fn();
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||
getAppEvents: () => ({
|
||||
publish: mockPublish,
|
||||
}),
|
||||
// getTemplateSrv: jest.fn().mockReturnValue({ replace: jest.fn(), getVariables: jest.fn() }),
|
||||
}));
|
||||
const mockDispatch = dispatch as jest.Mock;
|
||||
|
||||
/**
|
||||
* Simulate switching to text editor, changing the query and switching back to visual editor
|
||||
@@ -30,22 +33,28 @@ async function changeTarget(ctx: any, target: string): Promise<void> {
|
||||
|
||||
describe('Graphite actions', () => {
|
||||
const ctx = {
|
||||
datasource: {
|
||||
metricFindQuery: jest.fn(() => Promise.resolve([])),
|
||||
getFuncDefs: jest.fn(() => Promise.resolve(gfunc.getFuncDefs('1.0'))),
|
||||
getFuncDef: gfunc.getFuncDef,
|
||||
waitForFuncDefsLoaded: jest.fn(() => Promise.resolve(null)),
|
||||
createFuncInstance: gfunc.createFuncInstance,
|
||||
getTagsAutoComplete: jest.fn().mockReturnValue(Promise.resolve([])),
|
||||
getTagValuesAutoComplete: jest.fn().mockReturnValue(Promise.resolve([])),
|
||||
},
|
||||
target: { target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' },
|
||||
} as any;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
ctx.state = null;
|
||||
const instanceSettings = {
|
||||
url: '/api/datasources/proxy/1',
|
||||
name: 'graphiteProd',
|
||||
jsonData: {
|
||||
rollupIndicatorEnabled: true,
|
||||
},
|
||||
};
|
||||
ctx.datasource = new GraphiteDatasource(instanceSettings);
|
||||
ctx.datasource.metricFindQuery = jest.fn(() => Promise.resolve([]));
|
||||
ctx.datasource.getFuncDefs = jest.fn(() => Promise.resolve(gfunc.getFuncDefs('1.0')));
|
||||
ctx.datasource.getFuncDef = gfunc.getFuncDef;
|
||||
ctx.datasource.waitForFuncDefsLoaded = jest.fn(() => Promise.resolve(null));
|
||||
ctx.datasource.createFuncInstance = gfunc.createFuncInstance;
|
||||
ctx.datasource.getTagsAutoComplete = jest.fn().mockReturnValue(Promise.resolve([]));
|
||||
ctx.datasource.getTagValuesAutoComplete = jest.fn().mockReturnValue(Promise.resolve([]));
|
||||
ctx.state = { datasource: ctx.datasource };
|
||||
ctx.dispatch = createStore((state) => {
|
||||
ctx.state = state;
|
||||
});
|
||||
@@ -57,7 +66,7 @@ describe('Graphite actions', () => {
|
||||
refresh: jest.fn(),
|
||||
queries: [],
|
||||
//@ts-ignore
|
||||
templateSrv: new TemplateSrvStub(),
|
||||
templateSrv: getTemplateSrv(),
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -220,7 +229,8 @@ describe('Graphite actions', () => {
|
||||
});
|
||||
|
||||
it('limit is passed when getting list of tag values', async () => {
|
||||
await getTagValuesSelectables(ctx.state, { key: 'key', operator: '=', value: 'value' }, 1, 'test');
|
||||
ctx.state.templateSrv = { replace: jest.fn(), getVariables: jest.fn() };
|
||||
ctx.datasource = await getTagValuesSelectables(ctx.state, { key: 'key', operator: '=', value: 'value' }, 1, 'test');
|
||||
expect(ctx.state.datasource.getTagValuesAutoComplete).toHaveBeenCalledWith([], 'key', 'test', { limit: 5000 });
|
||||
});
|
||||
|
||||
@@ -237,20 +247,18 @@ describe('Graphite actions', () => {
|
||||
it('getAltSegmentsSelectables should handle autocomplete errors', async () => {
|
||||
await expect(async () => {
|
||||
await getAltSegmentsSelectables(ctx.state, 0, 'any');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'appNotifications/notifyApp',
|
||||
})
|
||||
expect(mockPublish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ payload: ['Fetching metrics failed: .'], type: 'alert-error' })
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('getAltSegmentsSelectables should display the error message only once', async () => {
|
||||
await getAltSegmentsSelectables(ctx.state, 0, 'any');
|
||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
||||
expect(mockPublish.mock.calls.length).toBe(1);
|
||||
|
||||
await getAltSegmentsSelectables(ctx.state, 0, 'any');
|
||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
||||
expect(mockPublish.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -267,39 +275,35 @@ describe('Graphite actions', () => {
|
||||
it('getTagsSelectables should handle autocomplete errors', async () => {
|
||||
await expect(async () => {
|
||||
await getTagsSelectables(ctx.state, 0, 'any');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'appNotifications/notifyApp',
|
||||
})
|
||||
expect(mockPublish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ payload: ['Fetching tags failed: .'], type: 'alert-error' })
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('getTagsSelectables should display the error message only once', async () => {
|
||||
await getTagsSelectables(ctx.state, 0, 'any');
|
||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
||||
expect(mockPublish.mock.calls.length).toBe(1);
|
||||
|
||||
await getTagsSelectables(ctx.state, 0, 'any');
|
||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
||||
expect(mockPublish.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('getTagsAsSegmentsSelectables should handle autocomplete errors', async () => {
|
||||
await expect(async () => {
|
||||
await getTagsAsSegmentsSelectables(ctx.state, 'any');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'appNotifications/notifyApp',
|
||||
})
|
||||
expect(mockPublish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ payload: ['Fetching tags failed: .'], type: 'alert-error' })
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('getTagsAsSegmentsSelectables should display the error message only once', async () => {
|
||||
await getTagsAsSegmentsSelectables(ctx.state, 'any');
|
||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
||||
expect(mockPublish.mock.calls.length).toBe(1);
|
||||
|
||||
await getTagsAsSegmentsSelectables(ctx.state, 'any');
|
||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
||||
expect(mockPublish.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -491,6 +495,7 @@ describe('Graphite actions', () => {
|
||||
|
||||
it('uses limited metrics and tags list', async () => {
|
||||
ctx.state.supportsTags = true;
|
||||
ctx.state.templateSrv = { replace: jest.fn(), getVariables: jest.fn() };
|
||||
const segments = await getAltSegmentsSelectables(ctx.state, 0, '');
|
||||
expect(segments).toHaveLength(10000);
|
||||
expect(segments[0].value!.value).toBe('*'); // * - is a fixed metric name, always added at the top
|
||||
@@ -501,6 +506,7 @@ describe('Graphite actions', () => {
|
||||
|
||||
it('uses correct limit for metrics and tags list when tags are not supported', async () => {
|
||||
ctx.state.supportsTags = false;
|
||||
ctx.state.templateSrv = { replace: jest.fn(), getVariables: jest.fn() };
|
||||
const segments = await getAltSegmentsSelectables(ctx.state, 0, '');
|
||||
expect(segments).toHaveLength(5000);
|
||||
expect(segments[0].value!.value).toBe('*'); // * - is a fixed metric name, always added at the top
|
||||
@@ -508,6 +514,7 @@ describe('Graphite actions', () => {
|
||||
});
|
||||
|
||||
it('uses limited metrics when adding more metrics', async () => {
|
||||
ctx.state.templateSrv = { replace: jest.fn(), getVariables: jest.fn() };
|
||||
const segments = await getAltSegmentsSelectables(ctx.state, 1, '');
|
||||
expect(segments).toHaveLength(5000);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createContext, Dispatch, PropsWithChildren, useContext, useEffect, useM
|
||||
import { usePrevious } from 'react-use';
|
||||
|
||||
import { QueryEditorProps } from '@grafana/data';
|
||||
import { getTemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
|
||||
import { GraphiteDatasource } from '../datasource';
|
||||
import { GraphiteOptions, GraphiteQuery } from '../types';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { clone, some } from 'lodash';
|
||||
|
||||
import { createErrorNotification } from '../../../../core/copy/appNotification';
|
||||
import { notifyApp } from '../../../../core/reducers/appNotification';
|
||||
import { dispatch } from '../../../../store/store';
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { getAppEvents } from '@grafana/runtime';
|
||||
|
||||
import { FuncInstance } from '../gfunc';
|
||||
import { GraphiteTagOperator } from '../types';
|
||||
|
||||
@@ -185,7 +185,10 @@ export function handleMetricsAutoCompleteError(
|
||||
): GraphiteQueryEditorState {
|
||||
if (!state.metricAutoCompleteErrorShown) {
|
||||
state.metricAutoCompleteErrorShown = true;
|
||||
dispatch(notifyApp(createErrorNotification(`Fetching metrics failed: ${error.message}.`)));
|
||||
getAppEvents().publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: [`Fetching metrics failed: ${error.message}.`],
|
||||
});
|
||||
}
|
||||
return state;
|
||||
}
|
||||
@@ -196,7 +199,10 @@ export function handleMetricsAutoCompleteError(
|
||||
export function handleTagsAutoCompleteError(state: GraphiteQueryEditorState, error: Error): GraphiteQueryEditorState {
|
||||
if (!state.tagsAutoCompleteErrorShown) {
|
||||
state.tagsAutoCompleteErrorShown = true;
|
||||
dispatch(notifyApp(createErrorNotification(`Fetching tags failed: ${error.message}.`)));
|
||||
getAppEvents().publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: [`Fetching tags failed: ${error.message}.`],
|
||||
});
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { AnyAction } from '@reduxjs/toolkit';
|
||||
import { Action, Dispatch } from 'redux';
|
||||
|
||||
import { DataQuery, TimeRange } from '@grafana/data';
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
|
||||
import { TemplateSrv } from '../../../../features/templating/template_srv';
|
||||
import { GraphiteDatasource } from '../datasource';
|
||||
import { FuncDefs } from '../gfunc';
|
||||
import GraphiteQuery, { GraphiteTarget } from '../graphite_query';
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"extends": "@grafana/plugin-configs/tsconfig.json",
|
||||
"include": ["."]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DataQuery, DataQueryRequest, DataSourceJsonData, TimeRange } from '@grafana/data';
|
||||
|
||||
import { TemplateSrv } from '../../../features/templating/template_srv';
|
||||
import { DataQueryRequest, DataSourceJsonData, TimeRange } from '@grafana/data';
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
|
||||
import { GraphiteDatasource } from './datasource';
|
||||
|
||||
|
||||
@@ -25,3 +25,8 @@ export function reduceError(error: any) {
|
||||
export function isGraphiteParserError(e: unknown): e is GraphiteParserError {
|
||||
return typeof e === 'object' && e !== null && 'message' in e && 'pos' in e;
|
||||
}
|
||||
|
||||
export const arrayMove = <T>(array: T[], fromIndex: number, toIndex: number): T[] => {
|
||||
array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]);
|
||||
return array;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import config from '@grafana/plugin-configs/webpack.config.ts';
|
||||
|
||||
// eslint-disable-next-line no-barrel-files/no-barrel-files
|
||||
export default config;
|
||||
@@ -2582,6 +2582,49 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@grafana-plugins/graphite@workspace:public/app/plugins/datasource/graphite":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@grafana-plugins/graphite@workspace:public/app/plugins/datasource/graphite"
|
||||
dependencies:
|
||||
"@emotion/css": "npm:11.13.5"
|
||||
"@grafana/data": "npm:12.2.0-pre"
|
||||
"@grafana/e2e-selectors": "npm:12.2.0-pre"
|
||||
"@grafana/plugin-configs": "npm:12.2.0-pre"
|
||||
"@grafana/plugin-ui": "npm:^0.10.10"
|
||||
"@grafana/runtime": "npm:12.2.0-pre"
|
||||
"@grafana/schema": "npm:12.2.0-pre"
|
||||
"@grafana/ui": "npm:12.2.0-pre"
|
||||
"@reduxjs/toolkit": "npm:2.8.2"
|
||||
"@testing-library/dom": "npm:10.4.1"
|
||||
"@testing-library/jest-dom": "npm:6.6.4"
|
||||
"@testing-library/react": "npm:16.3.0"
|
||||
"@testing-library/user-event": "npm:14.6.1"
|
||||
"@types/jest": "npm:29.5.14"
|
||||
"@types/lodash": "npm:4.17.20"
|
||||
"@types/node": "npm:22.17.0"
|
||||
"@types/react": "npm:18.3.18"
|
||||
"@types/react-dom": "npm:18.3.5"
|
||||
"@types/semver": "npm:7.7.0"
|
||||
"@types/uuid": "npm:10.0.0"
|
||||
jest: "npm:29.7.0"
|
||||
lodash: "npm:4.17.21"
|
||||
moment: "npm:2.30.1"
|
||||
react: "npm:18.3.1"
|
||||
react-dom: "npm:18.3.1"
|
||||
react-use: "npm:17.6.0"
|
||||
redux: "npm:5.0.1"
|
||||
rxjs: "npm:7.8.2"
|
||||
semver: "npm:7.7.2"
|
||||
ts-node: "npm:10.9.2"
|
||||
tslib: "npm:2.8.1"
|
||||
typescript: "npm:5.9.2"
|
||||
uuid: "npm:11.1.0"
|
||||
webpack: "npm:5.101.0"
|
||||
peerDependencies:
|
||||
"@grafana/runtime": "*"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@grafana-plugins/jaeger@workspace:public/app/plugins/datasource/jaeger":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@grafana-plugins/jaeger@workspace:public/app/plugins/datasource/jaeger"
|
||||
|
||||
Reference in New Issue
Block a user