Grafana: include a built-in backend datasource (#38571)

This commit is contained in:
Ryan McKinley 2021-09-10 07:44:47 -07:00 committed by GitHub
parent f74421b892
commit 6bda64cb19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 517 additions and 116 deletions

View File

@ -370,7 +370,6 @@ func (hs *HTTPServer) registerRoutes() {
// metrics
apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), routing.Wrap(hs.QueryMetrics))
apiRoute.Get("/tsdb/testdata/random-walk", routing.Wrap(hs.GetTestDataRandomWalk))
// DataSource w/ expressions
apiRoute.Post("/ds/query", bind(dtos.MetricRequest{}), routing.Wrap(hs.QueryMetricsV2))

View File

@ -5,6 +5,7 @@ import (
"strconv"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb/grafanads"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/util"
@ -112,11 +113,16 @@ func (hs *HTTPServer) getFSDataSources(c *models.ReqContext, enabledPlugins *plu
// the datasource table)
for _, ds := range hs.PluginManager.DataSources() {
if ds.BuiltIn {
dataSources[ds.Name] = map[string]interface{}{
info := map[string]interface{}{
"type": ds.Type,
"name": ds.Name,
"meta": hs.PluginManager.GetDataSource(ds.Id),
}
if ds.Name == grafanads.DatasourceName {
info["id"] = grafanads.DatasourceID
info["uid"] = grafanads.DatasourceUID
}
dataSources[ds.Name] = info
}
}

View File

@ -1,18 +1,17 @@
package api
import (
"context"
"errors"
"net/http"
"github.com/grafana/grafana/pkg/tsdb/grafanads"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/util"
)
// QueryMetricsV2 returns query metrics.
@ -31,31 +30,42 @@ func (hs *HTTPServer) QueryMetricsV2(c *models.ReqContext, reqDTO dtos.MetricReq
}
// Loop to see if we have an expression.
prevType := ""
var ds *models.DataSource
for _, query := range reqDTO.Queries {
if query.Get("datasource").MustString("") == expr.DatasourceName {
dsType := query.Get("datasource").MustString("")
if dsType == expr.DatasourceName {
return hs.handleExpressions(c, reqDTO)
}
}
var ds *models.DataSource
for i, query := range reqDTO.Queries {
hs.log.Debug("Processing metrics query", "query", query)
datasourceID, err := query.Get("datasourceId").Int64()
if err != nil {
if prevType != "" && prevType != dsType {
// For mixed datasource case, each data source is sent in a single request.
// So only the datasource from the first query is needed. As all requests
// should be the same data source.
hs.log.Debug("Can't process query since it's missing data source ID")
return response.Error(http.StatusBadRequest, "Query missing data source ID", nil)
return response.Error(http.StatusBadRequest, "All queries must use the same datasource", nil)
}
// For mixed datasource case, each data source is sent in a single request.
// So only the datasource from the first query is needed. As all requests
// should be the same data source.
if i == 0 {
ds, err = hs.DataSourceCache.GetDatasource(datasourceID, c.SignedInUser, c.SkipCache)
if ds == nil {
// require ID for everything
dsID, err := query.Get("datasourceId").Int64()
if err != nil {
return hs.handleGetDataSourceError(err, datasourceID)
hs.log.Debug("Can't process query since it's missing data source ID")
return response.Error(http.StatusBadRequest, "Query missing data source ID", nil)
}
if dsID == grafanads.DatasourceID {
ds = grafanads.DataSourceModel(c.OrgId)
} else {
ds, err = hs.DataSourceCache.GetDatasource(dsID, c.SignedInUser, c.SkipCache)
if err != nil {
return hs.handleGetDataSourceError(err, dsID)
}
}
}
prevType = dsType
}
for _, query := range reqDTO.Queries {
hs.log.Debug("Processing metrics query", "query", query)
request.Queries = append(request.Queries, plugins.DataSubQuery{
RefID: query.Get("refId").MustString("A"),
@ -212,37 +222,3 @@ func (hs *HTTPServer) QueryMetrics(c *models.ReqContext, reqDto dtos.MetricReque
return response.JSON(statusCode, &resp)
}
// GET /api/tsdb/testdata/random-walk
func (hs *HTTPServer) GetTestDataRandomWalk(c *models.ReqContext) response.Response {
from := c.Query("from")
to := c.Query("to")
intervalMS := c.QueryInt64("intervalMs")
timeRange := plugins.NewDataTimeRange(from, to)
request := plugins.DataQuery{TimeRange: &timeRange}
dsInfo := &models.DataSource{
Type: "testdata",
JsonData: simplejson.New(),
}
request.Queries = append(request.Queries, plugins.DataSubQuery{
RefID: "A",
IntervalMS: intervalMS,
Model: simplejson.NewFromAny(&util.DynMap{
"scenario": "random_walk",
}),
DataSource: dsInfo,
})
resp, err := hs.DataService.HandleRequest(context.Background(), dsInfo, request)
if err != nil {
return response.Error(500, "Metric request error", err)
}
qdr, err := resp.ToBackendDataResponse()
if err != nil {
return response.Error(http.StatusInternalServerError, "error converting results", err)
}
return toMacronResponse(qdr)
}

View File

@ -23,6 +23,7 @@ import (
"github.com/grafana/grafana/pkg/tsdb/azuremonitor"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch"
"github.com/grafana/grafana/pkg/tsdb/elasticsearch"
"github.com/grafana/grafana/pkg/tsdb/grafanads"
"github.com/grafana/grafana/pkg/tsdb/graphite"
"github.com/grafana/grafana/pkg/tsdb/influxdb"
"github.com/grafana/grafana/pkg/tsdb/loki"
@ -46,7 +47,7 @@ func ProvideBackgroundServiceRegistry(
_ *azuremonitor.Service, _ *cloudwatch.CloudWatchService, _ *elasticsearch.Service, _ *graphite.Service,
_ *influxdb.Service, _ *loki.Service, _ *opentsdb.Service, _ *prometheus.Service, _ *tempo.Service,
_ *testdatasource.TestDataPlugin, _ *plugindashboards.Service, _ *dashboardsnapshots.Service,
_ *postgres.Service, _ *mysql.Service, _ *mssql.Service,
_ *postgres.Service, _ *mysql.Service, _ *mssql.Service, _ *grafanads.Service,
) *BackgroundServiceRegistry {
return NewBackgroundServiceRegistry(

View File

@ -57,6 +57,7 @@ import (
"github.com/grafana/grafana/pkg/tsdb/cloudmonitoring"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch"
"github.com/grafana/grafana/pkg/tsdb/elasticsearch"
"github.com/grafana/grafana/pkg/tsdb/grafanads"
"github.com/grafana/grafana/pkg/tsdb/graphite"
"github.com/grafana/grafana/pkg/tsdb/influxdb"
"github.com/grafana/grafana/pkg/tsdb/loki"
@ -141,6 +142,7 @@ var wireBasicSet = wire.NewSet(
graphite.ProvideService,
prometheus.ProvideService,
elasticsearch.ProvideService,
grafanads.ProvideService,
dashboardsnapshots.ProvideService,
)

View File

@ -0,0 +1,230 @@
package grafanads
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/grafana/grafana/pkg/components/securejsondata"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana-plugin-sdk-go/experimental"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/testdatasource"
)
// DatasourceName is the string constant used as the datasource name in requests
// to identify it as a Grafana DS command.
const DatasourceName = "-- Grafana --"
// DatasourceID is the fake datasource id used in requests to identify it as a
// Grafana DS command.
const DatasourceID = -1
// DatasourceUID is the fake datasource uid used in requests to identify it as a
// Grafana DS command.
const DatasourceUID = "grafana"
// Make sure Service implements required interfaces.
// This is important to do since otherwise we will only get a
// not implemented error response from plugin at runtime.
var (
_ backend.QueryDataHandler = (*Service)(nil)
_ backend.CheckHealthHandler = (*Service)(nil)
logger = log.New("tsdb.grafana")
)
func ProvideService(cfg *setting.Cfg, backendPM backendplugin.Manager) *Service {
return newService(cfg.StaticRootPath, backendPM)
}
func newService(staticRootPath string, backendPM backendplugin.Manager) *Service {
s := &Service{
staticRootPath: staticRootPath,
roots: []string{
"testdata",
"img/icons",
"img/bg",
"gazetteer",
"upload", // does not exist yet
},
}
if err := backendPM.Register("grafana", coreplugin.New(backend.ServeOpts{
CheckHealthHandler: s,
QueryDataHandler: s,
})); err != nil {
logger.Error("Failed to register plugin", "error", err)
return nil
}
return s
}
// Service exists regardless of user settings
type Service struct {
// path to the public folder
staticRootPath string
roots []string
}
func DataSourceModel(orgId int64) *models.DataSource {
return &models.DataSource{
Id: DatasourceID,
Uid: DatasourceUID,
Name: DatasourceName,
Type: "grafana",
OrgId: orgId,
JsonData: simplejson.New(),
SecureJsonData: make(securejsondata.SecureJsonData),
}
}
func (s *Service) QueryData(_ context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
response := backend.NewQueryDataResponse()
for _, q := range req.Queries {
switch q.QueryType {
case queryTypeRandomWalk:
response.Responses[q.RefID] = s.doRandomWalk(q)
case queryTypeList:
response.Responses[q.RefID] = s.doListQuery(q)
case queryTypeRead:
response.Responses[q.RefID] = s.doReadQuery(q)
default:
response.Responses[q.RefID] = backend.DataResponse{
Error: fmt.Errorf("unknown query type"),
}
}
}
return response, nil
}
func (s *Service) CheckHealth(_ context.Context, _ *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
return &backend.CheckHealthResult{
Status: backend.HealthStatusOk,
Message: "OK",
}, nil
}
func (s *Service) publicPath(path string) (string, error) {
if strings.Contains(path, "..") {
return "", fmt.Errorf("invalid string")
}
ok := false
for _, root := range s.roots {
if strings.HasPrefix(path, root) {
ok = true
break
}
}
if !ok {
return "", fmt.Errorf("bad root path")
}
return filepath.Join(s.staticRootPath, path), nil
}
func (s *Service) doListQuery(query backend.DataQuery) backend.DataResponse {
q := &listQueryModel{}
response := backend.DataResponse{}
err := json.Unmarshal(query.JSON, &q)
if err != nil {
response.Error = err
return response
}
if q.Path == "" {
count := len(s.roots)
names := data.NewFieldFromFieldType(data.FieldTypeString, count)
mtype := data.NewFieldFromFieldType(data.FieldTypeString, count)
names.Name = "name"
mtype.Name = "mediaType"
for i, f := range s.roots {
names.Set(i, f)
mtype.Set(i, "directory")
}
frame := data.NewFrame("", names, mtype)
frame.SetMeta(&data.FrameMeta{
Type: data.FrameTypeDirectoryListing,
})
response.Frames = data.Frames{frame}
} else {
path, err := s.publicPath(q.Path)
if err != nil {
response.Error = err
return response
}
frame, err := experimental.GetDirectoryFrame(path, false)
if err != nil {
response.Error = err
return response
}
response.Frames = data.Frames{frame}
}
return response
}
func (s *Service) doReadQuery(query backend.DataQuery) backend.DataResponse {
q := &listQueryModel{}
response := backend.DataResponse{}
err := json.Unmarshal(query.JSON, &q)
if err != nil {
response.Error = err
return response
}
if filepath.Ext(q.Path) != ".csv" {
response.Error = fmt.Errorf("unsupported file type")
return response
}
path, err := s.publicPath(q.Path)
if err != nil {
response.Error = err
return response
}
// Can ignore gosec G304 here, because we check the file pattern above
// nolint:gosec
fileReader, err := os.Open(path)
if err != nil {
response.Error = fmt.Errorf("failed to read file")
return response
}
defer func() {
if err := fileReader.Close(); err != nil {
logger.Warn("Failed to close file", "err", err, "path", path)
}
}()
frame, err := testdatasource.LoadCsvContent(fileReader, filepath.Base(path))
if err != nil {
response.Error = err
return response
}
response.Frames = data.Frames{frame}
return response
}
func (s *Service) doRandomWalk(query backend.DataQuery) backend.DataResponse {
response := backend.DataResponse{}
model := simplejson.New()
response.Frames = data.Frames{testdatasource.RandomWalk(query, model, 0)}
return response
}

View File

@ -0,0 +1,50 @@
package grafanads
import (
"encoding/json"
"path"
"testing"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/experimental"
"github.com/stretchr/testify/require"
)
func asJSON(v interface{}) json.RawMessage {
b, _ := json.Marshal(v)
return b
}
func TestReadFolderListing(t *testing.T) {
ds := newService("../../../public", &fakeBackendPM{})
dr := ds.doListQuery(backend.DataQuery{
QueryType: "x",
JSON: asJSON(listQueryModel{
Path: "testdata",
}),
})
err := experimental.CheckGoldenDataResponse(path.Join("testdata", "list.golden.txt"), &dr, true)
require.NoError(t, err)
}
func TestReadCSVFile(t *testing.T) {
ds := newService("../../../public", &fakeBackendPM{})
dr := ds.doReadQuery(backend.DataQuery{
QueryType: "x",
JSON: asJSON(readQueryModel{
Path: "testdata/js_libraries.csv",
}),
})
err := experimental.CheckGoldenDataResponse(path.Join("testdata", "jslib.golden.txt"), &dr, true)
require.NoError(t, err)
}
type fakeBackendPM struct {
backendplugin.Manager
}
func (pm *fakeBackendPM) Register(pluginID string, factory backendplugin.PluginFactoryFunc) error {
return nil
}

View File

@ -0,0 +1,21 @@
package grafanads
const (
// QueryTypeRandomWalk returns a random walk series
queryTypeRandomWalk = "randomWalk"
// QueryTypeList will list the files in a folder
queryTypeList = "list"
// QueryTypeRead will read a file and return it as data frames
// currently only .csv files are supported,
// other file types will eventually be supported (parquet, etc)
queryTypeRead = "read"
)
type listQueryModel struct {
Path string `json:"path"`
}
type readQueryModel struct {
Path string `json:"path"`
}

View File

@ -0,0 +1,21 @@
🌟 This was machine generated. Do not edit. 🌟
Frame[0]
Name: js_libraries.csv
Dimensions: 4 Fields by 6 Rows
+-----------------+--------------------+----------------+----------------+
| Name: Library | Name: Github Stars | Name: Forks | Name: Watchers |
| Labels: | Labels: | Labels: | Labels: |
| Type: []*string | Type: []*int64 | Type: []*int64 | Type: []*int64 |
+-----------------+--------------------+----------------+----------------+
| React.js | 169000 | 34000 | 6700 |
| Vue | 184000 | 29100 | 6300 |
| Angular | 73400 | 19300 | 3200 |
| JQuery | 54900 | 20000 | 3300 |
| Meteor | 42400 | 5200 | 1700 |
| Aurelia | 11600 | 684 | 442 |
+-----------------+--------------------+----------------+----------------+
====== TEST DATA RESPONSE (arrow base64) ======
FRAME=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

View File

@ -0,0 +1,24 @@
🌟 This was machine generated. Do not edit. 🌟
Frame[0] {
"type": "directory-listing",
"pathSeparator": "/"
}
Name:
Dimensions: 2 Fields by 6 Rows
+--------------------------+------------------+
| Name: name | Name: media-type |
| Labels: | Labels: |
| Type: []string | Type: []string |
+--------------------------+------------------+
| browser_marketshare.csv | |
| flight_info_by_state.csv | |
| gdp_per_capita.csv | |
| js_libraries.csv | |
| population_by_state.csv | |
| weight_height.csv | |
+--------------------------+------------------+
====== TEST DATA RESPONSE (arrow base64) ======
FRAME=QVJST1cxAAD/////uAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAKQAAAADAAAATAAAACgAAAAEAAAA0P7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADw/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAABD///8IAAAAPAAAADAAAAB7InR5cGUiOiJkaXJlY3RvcnktbGlzdGluZyIsInBhdGhTZXBhcmF0b3IiOiIvIn0AAAAABAAAAG1ldGEAAAAAAgAAAHwAAAAEAAAAnv///xQAAABAAAAAQAAAAAAAAAU8AAAAAQAAAAQAAACM////CAAAABQAAAAKAAAAbWVkaWEtdHlwZQAABAAAAG5hbWUAAAAAAAAAAIj///8KAAAAbWVkaWEtdHlwZQAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABIAAAAAAAABUQAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAG5hbWUAAAAABAAAAG5hbWUAAAAAAAAAAAQABAAEAAAABAAAAG5hbWUAAAAA/////9gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAADAAAAAAAAAABQAAAAAAAADAwAKABgADAAIAAQACgAAABQAAAB4AAAABgAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAACAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAACAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAACAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAFwAAAC8AAABBAAAAUQAAAGgAAAB5AAAAAAAAAGJyb3dzZXJfbWFya2V0c2hhcmUuY3N2ZmxpZ2h0X2luZm9fYnlfc3RhdGUuY3N2Z2RwX3Blcl9jYXBpdGEuY3N2anNfbGlicmFyaWVzLmNzdnBvcHVsYXRpb25fYnlfc3RhdGUuY3N2d2VpZ2h0X2hlaWdodC5jc3YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAPAAAAAAAAwABAAAAyAEAAAAAAADgAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACkAAAAAwAAAEwAAAAoAAAABAAAAND+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAA8P7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAAAQ////CAAAADwAAAAwAAAAeyJ0eXBlIjoiZGlyZWN0b3J5LWxpc3RpbmciLCJwYXRoU2VwYXJhdG9yIjoiLyJ9AAAAAAQAAABtZXRhAAAAAAIAAAB8AAAABAAAAJ7///8UAAAAQAAAAEAAAAAAAAAFPAAAAAEAAAAEAAAAjP///wgAAAAUAAAACgAAAG1lZGlhLXR5cGUAAAQAAABuYW1lAAAAAAAAAACI////CgAAAG1lZGlhLXR5cGUAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAASAAAAAAAAAVEAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABuYW1lAAAAAAQAAABuYW1lAAAAAAAAAAAEAAQABAAAAAQAAABuYW1lAAAAAOgBAABBUlJPVzE=

View File

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
@ -16,8 +15,11 @@ import (
// NewService returns a new Service.
func NewService(
cfg *setting.Cfg, pluginManager plugins.Manager, backendPluginManager backendplugin.Manager,
oauthTokenService *oauthtoken.Service, httpClientProvider httpclient.Provider, cloudMonitoringService *cloudmonitoring.Service,
cfg *setting.Cfg,
pluginManager plugins.Manager,
backendPluginManager backendplugin.Manager,
oauthTokenService *oauthtoken.Service,
cloudMonitoringService *cloudmonitoring.Service,
) *Service {
s := newService(cfg, pluginManager, backendPluginManager, oauthTokenService)
@ -46,7 +48,6 @@ type Service struct {
PluginManager plugins.Manager
BackendPluginManager backendplugin.Manager
OAuthTokenService oauthtoken.OAuthTokenService
//nolint: staticcheck // plugins.DataPlugin deprecated
registry map[string]func(*models.DataSource) (plugins.DataPlugin, error)
}
@ -64,7 +65,6 @@ func (s *Service) HandleRequest(ctx context.Context, ds *models.DataSource, quer
return plugin.DataQuery(ctx, ds, query)
}
return dataPluginQueryAdapter(ds.Type, s.BackendPluginManager, s.OAuthTokenService).DataQuery(ctx, ds, query)
}

View File

@ -142,6 +142,7 @@ func createService() (*Service, *fakeExecutor, *fakeBackendPM) {
manager := &manager.PluginManager{
BackendPluginManager: fakeBackendPM,
}
s := newService(setting.NewCfg(), manager, fakeBackendPM, &fakeOAuthTokenService{})
e := &fakeExecutor{
//nolint: staticcheck // plugins.DataPlugin deprecated

View File

@ -30,7 +30,7 @@ func (p *TestDataPlugin) handleCsvContentScenario(ctx context.Context, req *back
csvContent := model.Get("csvContent").MustString()
alias := model.Get("alias").MustString("")
frame, err := p.loadCsvContent(strings.NewReader(csvContent), alias)
frame, err := LoadCsvContent(strings.NewReader(csvContent), alias)
if err != nil {
return nil, err
}
@ -94,10 +94,11 @@ func (p *TestDataPlugin) loadCsvFile(fileName string) (*data.Frame, error) {
}
}()
return p.loadCsvContent(fileReader, fileName)
return LoadCsvContent(fileReader, fileName)
}
func (p *TestDataPlugin) loadCsvContent(ioReader io.Reader, name string) (*data.Frame, error) {
// LoadCsvContent should be moved to the SDK
func LoadCsvContent(ioReader io.Reader, name string) (*data.Frame, error) {
reader := csv.NewReader(ioReader)
// Read the header records

View File

@ -35,7 +35,7 @@ func TestCSVFileScenario(t *testing.T) {
_ = fileReader.Close()
}()
frame, err := p.loadCsvContent(fileReader, name)
frame, err := LoadCsvContent(fileReader, name)
require.NoError(t, err)
require.NotNil(t, frame)

View File

@ -268,7 +268,7 @@ func (p *TestDataPlugin) handleRandomWalkScenario(ctx context.Context, req *back
for i := 0; i < seriesCount; i++ {
respD := resp.Responses[q.RefID]
respD.Frames = append(respD.Frames, randomWalk(q, model, i))
respD.Frames = append(respD.Frames, RandomWalk(q, model, i))
resp.Responses[q.RefID] = respD
}
}
@ -354,7 +354,7 @@ func (p *TestDataPlugin) handleRandomWalkWithErrorScenario(ctx context.Context,
}
respD := resp.Responses[q.RefID]
respD.Frames = append(respD.Frames, randomWalk(q, model, 0))
respD.Frames = append(respD.Frames, RandomWalk(q, model, 0))
respD.Error = fmt.Errorf("this is an error and it can include URLs http://grafana.com/")
resp.Responses[q.RefID] = respD
}
@ -376,7 +376,7 @@ func (p *TestDataPlugin) handleRandomWalkSlowScenario(ctx context.Context, req *
time.Sleep(parsedInterval)
respD := resp.Responses[q.RefID]
respD.Frames = append(respD.Frames, randomWalk(q, model, 0))
respD.Frames = append(respD.Frames, RandomWalk(q, model, 0))
resp.Responses[q.RefID] = respD
}
@ -618,7 +618,7 @@ func (p *TestDataPlugin) handleLogsScenario(ctx context.Context, req *backend.Qu
return resp, nil
}
func randomWalk(query backend.DataQuery, model *simplejson.Json, index int) *data.Frame {
func RandomWalk(query backend.DataQuery, model *simplejson.Json, index int) *data.Frame {
timeWalkerMs := query.TimeRange.From.UnixNano() / int64(time.Millisecond)
to := query.TimeRange.To.UnixNano() / int64(time.Millisecond)
startValue := model.Get("startValue").MustFloat64(rand.Float64() * 100)

View File

@ -1,9 +1,16 @@
import React, { PureComponent } from 'react';
import { InlineField, Select, Alert, Input } from '@grafana/ui';
import { QueryEditorProps, SelectableValue, dataFrameFromJSON, rangeUtil } from '@grafana/data';
import { InlineField, Select, Alert, Input, InlineFieldRow } from '@grafana/ui';
import {
QueryEditorProps,
SelectableValue,
dataFrameFromJSON,
rangeUtil,
DataQueryRequest,
DataFrame,
} from '@grafana/data';
import { GrafanaDatasource } from '../datasource';
import { defaultQuery, GrafanaQuery, GrafanaQueryType } from '../types';
import { getBackendSrv } from '@grafana/runtime';
import { getBackendSrv, getDataSourceSrv } from '@grafana/runtime';
type Props = QueryEditorProps<GrafanaDatasource, GrafanaQuery>;
@ -12,6 +19,7 @@ const labelWidth = 12;
interface State {
channels: Array<SelectableValue<string>>;
channelFields: Record<string, Array<SelectableValue<string>>>;
folders?: Array<SelectableValue<string>>;
}
export class QueryEditor extends PureComponent<Props, State> {
@ -28,6 +36,11 @@ export class QueryEditor extends PureComponent<Props, State> {
value: GrafanaQueryType.LiveMeasurements,
description: 'Stream real-time measurements from Grafana',
},
{
label: 'List public files',
value: GrafanaQueryType.List,
description: 'Show directory listings for public resources',
},
];
loadChannelInfo() {
@ -62,6 +75,30 @@ export class QueryEditor extends PureComponent<Props, State> {
});
}
loadFolderInfo() {
const query: DataQueryRequest<GrafanaQuery> = {
targets: [{ queryType: GrafanaQueryType.List, refId: 'A' }],
} as any;
getDataSourceSrv()
.get('-- Grafana --')
.then((ds) => {
const gds = ds as GrafanaDatasource;
gds.query(query).subscribe({
next: (rsp) => {
if (rsp.data.length) {
const names = (rsp.data[0] as DataFrame).fields[0];
const folders = names.values.toArray().map((v) => ({
value: v,
label: v,
}));
this.setState({ folders });
}
},
});
});
}
componentDidMount() {
this.loadChannelInfo();
}
@ -242,6 +279,49 @@ export class QueryEditor extends PureComponent<Props, State> {
);
}
onFolderChanged = (sel: SelectableValue<string>) => {
const { onChange, query, onRunQuery } = this.props;
onChange({ ...query, path: sel?.value });
onRunQuery();
};
renderListPublicFiles() {
let { path } = this.props.query;
let { folders } = this.state;
if (!folders) {
folders = [];
this.loadFolderInfo();
}
const currentFolder = folders.find((f) => f.value === path);
if (path && !currentFolder) {
folders = [
...folders,
{
value: path,
label: path,
},
];
}
return (
<InlineFieldRow>
<InlineField label="Path" grow={true} labelWidth={labelWidth}>
<Select
menuShouldPortal
options={folders}
value={currentFolder || ''}
onChange={this.onFolderChanged}
allowCustomValue={true}
backspaceRemovesValue={true}
placeholder="Select folder"
isClearable={true}
formatCreateLabel={(input: string) => `Folder: ${input}`}
/>
</InlineField>
</InlineFieldRow>
);
}
render() {
const query = {
...defaultQuery,
@ -250,7 +330,7 @@ export class QueryEditor extends PureComponent<Props, State> {
return (
<>
<div className="gf-form">
<InlineFieldRow>
<InlineField label="Query type" grow={true} labelWidth={labelWidth}>
<Select
menuShouldPortal
@ -259,8 +339,9 @@ export class QueryEditor extends PureComponent<Props, State> {
onChange={this.onQueryTypeChange}
/>
</InlineField>
</div>
</InlineFieldRow>
{query.queryType === GrafanaQueryType.LiveMeasurements && this.renderMeasurementsQuery()}
{query.queryType === GrafanaQueryType.List && this.renderListPublicFiles()}
</>
);
}

View File

@ -1,12 +1,10 @@
import { from, merge, Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { getBackendSrv, getGrafanaLiveSrv, getTemplateSrv, toDataQueryResponse } from '@grafana/runtime';
import { DataSourceWithBackend, getBackendSrv, getGrafanaLiveSrv, getTemplateSrv } from '@grafana/runtime';
import {
AnnotationQuery,
AnnotationQueryRequest,
DataQueryRequest,
DataQueryResponse,
DataSourceApi,
DataSourceInstanceSettings,
DatasourceRef,
isValidLiveChannelAddress,
@ -22,7 +20,7 @@ import { isString } from 'lodash';
let counter = 100;
export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
export class GrafanaDatasource extends DataSourceWithBackend<GrafanaQuery> {
constructor(instanceSettings: DataSourceInstanceSettings) {
super(instanceSettings);
this.annotations = {
@ -49,7 +47,8 @@ export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
}
query(request: DataQueryRequest<GrafanaQuery>): Observable<DataQueryResponse> {
const queries: Array<Observable<DataQueryResponse>> = [];
const results: Array<Observable<DataQueryResponse>> = [];
const targets: GrafanaQuery[] = [];
const templateSrv = getTemplateSrv();
for (const target of request.targets) {
if (target.queryType === GrafanaQueryType.Annotations) {
@ -90,7 +89,7 @@ export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
buffer.maxDelta = request.range.to.valueOf() - request.range.from.valueOf();
}
queries.push(
results.push(
getGrafanaLiveSrv().getDataStream({
key: `${request.requestId}.${counter++}`,
addr: addr!,
@ -99,15 +98,28 @@ export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
})
);
} else {
queries.push(getRandomWalk(request));
if (!target.queryType) {
target.queryType = GrafanaQueryType.RandomWalk;
}
targets.push(target);
}
}
// With a single query just return the results
if (queries.length === 1) {
return queries[0];
if (targets.length) {
results.push(
super.query({
...request,
targets,
})
);
}
if (queries.length > 1) {
return merge(...queries);
if (results.length) {
// With a single query just return the results
if (results.length === 1) {
return results[0];
}
return merge(...results);
}
return of(); // nothing
}
@ -171,32 +183,3 @@ export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
return Promise.resolve();
}
}
// Note that the query does not actually matter
function getRandomWalk(request: DataQueryRequest): Observable<DataQueryResponse> {
const { intervalMs, maxDataPoints, range, requestId } = request;
// Yes, this implementation ignores multiple targets! But that matches existing behavior
const params: Record<string, any> = {
intervalMs,
maxDataPoints,
from: range.from.valueOf(),
to: range.to.valueOf(),
};
return getBackendSrv()
.fetch({
url: '/api/tsdb/testdata/random-walk',
method: 'GET',
params,
requestId,
})
.pipe(
map((rsp: any) => {
return toDataQueryResponse(rsp);
}),
catchError((err) => {
return of(toDataQueryResponse(err));
})
);
}

View File

@ -6,9 +6,13 @@ import { LiveDataFilter } from '@grafana/runtime';
//----------------------------------------------
export enum GrafanaQueryType {
RandomWalk = 'randomWalk',
LiveMeasurements = 'measurements',
Annotations = 'annotations',
// backend
RandomWalk = 'randomWalk',
List = 'list',
Read = 'read',
}
export interface GrafanaQuery extends DataQuery {
@ -16,6 +20,7 @@ export interface GrafanaQuery extends DataQuery {
channel?: string;
filter?: LiveDataFilter;
buffer?: number;
path?: string; // for list and read
}
export const defaultQuery: GrafanaQuery = {