mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Provisioning: Allows specifying uid for datasource and use that in derived fields (#23585)
* Add uid to datasource * Fix uid passing when provisioning * Better error handling and Uid column type change * Fix test and strict null error counts * Add backend tests * Add tests * Fix strict null checks * Update test * Improve tests * Update pkg/services/sqlstore/datasource.go Co-Authored-By: Arve Knudsen <arve.knudsen@gmail.com> * Variable rename Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
parent
d5f8d976f0
commit
e5dd7efdee
@ -29,7 +29,9 @@ export interface DataLink {
|
|||||||
onClick?: (event: DataLinkClickEvent) => void;
|
onClick?: (event: DataLinkClickEvent) => void;
|
||||||
|
|
||||||
// At the moment this is used for derived fields for metadata about internal linking.
|
// At the moment this is used for derived fields for metadata about internal linking.
|
||||||
meta?: any;
|
meta?: {
|
||||||
|
datasourceUid?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LinkTarget = '_blank' | '_self';
|
export type LinkTarget = '_blank' | '_self';
|
||||||
|
@ -505,6 +505,7 @@ export interface DataSourceSettings<T extends DataSourceJsonData = DataSourceJso
|
|||||||
*/
|
*/
|
||||||
export interface DataSourceInstanceSettings<T extends DataSourceJsonData = DataSourceJsonData> {
|
export interface DataSourceInstanceSettings<T extends DataSourceJsonData = DataSourceJsonData> {
|
||||||
id: number;
|
id: number;
|
||||||
|
uid: string;
|
||||||
type: string;
|
type: string;
|
||||||
name: string;
|
name: string;
|
||||||
meta: DataSourcePluginMeta;
|
meta: DataSourcePluginMeta;
|
||||||
|
@ -7,7 +7,8 @@ export interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
|||||||
label: string;
|
label: string;
|
||||||
tooltip?: PopoverContent;
|
tooltip?: PopoverContent;
|
||||||
labelWidth?: number;
|
labelWidth?: number;
|
||||||
inputWidth?: number;
|
// If null no width will be specified not even default one
|
||||||
|
inputWidth?: number | null;
|
||||||
inputEl?: React.ReactNode;
|
inputEl?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,7 +128,7 @@ func AddDataSource(c *models.ReqContext, cmd models.AddDataSourceCommand) Respon
|
|||||||
cmd.OrgId = c.OrgId
|
cmd.OrgId = c.OrgId
|
||||||
|
|
||||||
if err := bus.Dispatch(&cmd); err != nil {
|
if err := bus.Dispatch(&cmd); err != nil {
|
||||||
if err == models.ErrDataSourceNameExists {
|
if err == models.ErrDataSourceNameExists || err == models.ErrDataSourceUidExists {
|
||||||
return Error(409, err.Error(), err)
|
return Error(409, err.Error(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,6 +67,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
|||||||
|
|
||||||
var dsMap = map[string]interface{}{
|
var dsMap = map[string]interface{}{
|
||||||
"id": ds.Id,
|
"id": ds.Id,
|
||||||
|
"uid": ds.Uid,
|
||||||
"type": ds.Type,
|
"type": ds.Type,
|
||||||
"name": ds.Name,
|
"name": ds.Name,
|
||||||
"url": url,
|
"url": url,
|
||||||
|
@ -19,10 +19,10 @@ func TestPasswordMigrationCommand(t *testing.T) {
|
|||||||
defer session.Close()
|
defer session.Close()
|
||||||
|
|
||||||
datasources := []*models.DataSource{
|
datasources := []*models.DataSource{
|
||||||
{Type: "influxdb", Name: "influxdb", Password: "foobar"},
|
{Type: "influxdb", Name: "influxdb", Password: "foobar", Uid: "influx"},
|
||||||
{Type: "graphite", Name: "graphite", BasicAuthPassword: "foobar"},
|
{Type: "graphite", Name: "graphite", BasicAuthPassword: "foobar", Uid: "graphite"},
|
||||||
{Type: "prometheus", Name: "prometheus"},
|
{Type: "prometheus", Name: "prometheus", Uid: "prom"},
|
||||||
{Type: "elasticsearch", Name: "elasticsearch", Password: "pwd"},
|
{Type: "elasticsearch", Name: "elasticsearch", Password: "pwd", Uid: "elastic"},
|
||||||
}
|
}
|
||||||
|
|
||||||
// set required default values
|
// set required default values
|
||||||
|
@ -28,11 +28,13 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrDataSourceNotFound = errors.New("Data source not found")
|
ErrDataSourceNotFound = errors.New("Data source not found")
|
||||||
ErrDataSourceNameExists = errors.New("Data source with same name already exists")
|
ErrDataSourceNameExists = errors.New("Data source with the same name already exists")
|
||||||
ErrDataSourceUpdatingOldVersion = errors.New("Trying to update old version of datasource")
|
ErrDataSourceUidExists = errors.New("Data source with the same uid already exists")
|
||||||
ErrDatasourceIsReadOnly = errors.New("Data source is readonly. Can only be updated from configuration")
|
ErrDataSourceUpdatingOldVersion = errors.New("Trying to update old version of datasource")
|
||||||
ErrDataSourceAccessDenied = errors.New("Data source access denied")
|
ErrDatasourceIsReadOnly = errors.New("Data source is readonly. Can only be updated from configuration")
|
||||||
|
ErrDataSourceAccessDenied = errors.New("Data source access denied")
|
||||||
|
ErrDataSourceFailedGenerateUniqueUid = errors.New("Failed to generate unique datasource id")
|
||||||
)
|
)
|
||||||
|
|
||||||
type DsAccess string
|
type DsAccess string
|
||||||
@ -57,6 +59,7 @@ type DataSource struct {
|
|||||||
JsonData *simplejson.Json
|
JsonData *simplejson.Json
|
||||||
SecureJsonData securejsondata.SecureJsonData
|
SecureJsonData securejsondata.SecureJsonData
|
||||||
ReadOnly bool
|
ReadOnly bool
|
||||||
|
Uid string
|
||||||
|
|
||||||
Created time.Time
|
Created time.Time
|
||||||
Updated time.Time
|
Updated time.Time
|
||||||
@ -144,6 +147,7 @@ type AddDataSourceCommand struct {
|
|||||||
IsDefault bool `json:"isDefault"`
|
IsDefault bool `json:"isDefault"`
|
||||||
JsonData *simplejson.Json `json:"jsonData"`
|
JsonData *simplejson.Json `json:"jsonData"`
|
||||||
SecureJsonData map[string]string `json:"secureJsonData"`
|
SecureJsonData map[string]string `json:"secureJsonData"`
|
||||||
|
Uid string `json:"uid"`
|
||||||
|
|
||||||
OrgId int64 `json:"-"`
|
OrgId int64 `json:"-"`
|
||||||
ReadOnly bool `json:"-"`
|
ReadOnly bool `json:"-"`
|
||||||
@ -168,6 +172,7 @@ type UpdateDataSourceCommand struct {
|
|||||||
JsonData *simplejson.Json `json:"jsonData"`
|
JsonData *simplejson.Json `json:"jsonData"`
|
||||||
SecureJsonData map[string]string `json:"secureJsonData"`
|
SecureJsonData map[string]string `json:"secureJsonData"`
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
|
Uid string `json:"uid"`
|
||||||
|
|
||||||
OrgId int64 `json:"-"`
|
OrgId int64 `json:"-"`
|
||||||
Id int64 `json:"-"`
|
Id int64 `json:"-"`
|
||||||
|
@ -161,7 +161,7 @@ func TestDatasourceAsConfig(t *testing.T) {
|
|||||||
|
|
||||||
So(dsCfg.APIVersion, ShouldEqual, 1)
|
So(dsCfg.APIVersion, ShouldEqual, 1)
|
||||||
|
|
||||||
validateDatasource(dsCfg)
|
validateDatasourceV1(dsCfg)
|
||||||
validateDeleteDatasources(dsCfg)
|
validateDeleteDatasources(dsCfg)
|
||||||
|
|
||||||
dsCount := 0
|
dsCount := 0
|
||||||
@ -231,6 +231,12 @@ func validateDatasource(dsCfg *configs) {
|
|||||||
So(ds.SecureJSONData["tlsClientKey"], ShouldEqual, "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A==")
|
So(ds.SecureJSONData["tlsClientKey"], ShouldEqual, "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A==")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateDatasourceV1(dsCfg *configs) {
|
||||||
|
validateDatasource(dsCfg)
|
||||||
|
ds := dsCfg.Datasources[0]
|
||||||
|
So(ds.UID, ShouldEqual, "test_uid")
|
||||||
|
}
|
||||||
|
|
||||||
type fakeRepository struct {
|
type fakeRepository struct {
|
||||||
inserted []*models.AddDataSourceCommand
|
inserted []*models.AddDataSourceCommand
|
||||||
deleted []*models.DeleteDataSourceByNameCommand
|
deleted []*models.DeleteDataSourceByNameCommand
|
||||||
|
@ -50,13 +50,13 @@ func (dc *DatasourceProvisioner) apply(cfg *configs) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err == models.ErrDataSourceNotFound {
|
if err == models.ErrDataSourceNotFound {
|
||||||
dc.log.Info("inserting datasource from configuration ", "name", ds.Name)
|
dc.log.Info("inserting datasource from configuration ", "name", ds.Name, "uid", ds.UID)
|
||||||
insertCmd := createInsertCommand(ds)
|
insertCmd := createInsertCommand(ds)
|
||||||
if err := bus.Dispatch(insertCmd); err != nil {
|
if err := bus.Dispatch(insertCmd); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dc.log.Debug("updating datasource from configuration", "name", ds.Name)
|
dc.log.Debug("updating datasource from configuration", "name", ds.Name, "uid", ds.UID)
|
||||||
updateCmd := createUpdateCommand(ds, cmd.Result.Id)
|
updateCmd := createUpdateCommand(ds, cmd.Result.Id)
|
||||||
if err := bus.Dispatch(updateCmd); err != nil {
|
if err := bus.Dispatch(updateCmd); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -24,6 +24,7 @@ datasources:
|
|||||||
tlsClientKey: "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A=="
|
tlsClientKey: "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A=="
|
||||||
editable: true
|
editable: true
|
||||||
version: 10
|
version: 10
|
||||||
|
uid: "test_uid"
|
||||||
|
|
||||||
deleteDatasources:
|
deleteDatasources:
|
||||||
- name: old-graphite3
|
- name: old-graphite3
|
||||||
|
@ -43,6 +43,7 @@ type upsertDataSourceFromConfig struct {
|
|||||||
JSONData map[string]interface{}
|
JSONData map[string]interface{}
|
||||||
SecureJSONData map[string]string
|
SecureJSONData map[string]string
|
||||||
Editable bool
|
Editable bool
|
||||||
|
UID string
|
||||||
}
|
}
|
||||||
|
|
||||||
type configsV0 struct {
|
type configsV0 struct {
|
||||||
@ -108,6 +109,7 @@ type upsertDataSourceFromConfigV1 struct {
|
|||||||
JSONData values.JSONValue `json:"jsonData" yaml:"jsonData"`
|
JSONData values.JSONValue `json:"jsonData" yaml:"jsonData"`
|
||||||
SecureJSONData values.StringMapValue `json:"secureJsonData" yaml:"secureJsonData"`
|
SecureJSONData values.StringMapValue `json:"secureJsonData" yaml:"secureJsonData"`
|
||||||
Editable values.BoolValue `json:"editable" yaml:"editable"`
|
Editable values.BoolValue `json:"editable" yaml:"editable"`
|
||||||
|
UID values.StringValue `json:"uid" yaml:"uid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *configsV1) mapToDatasourceFromConfig(apiVersion int64) *configs {
|
func (cfg *configsV1) mapToDatasourceFromConfig(apiVersion int64) *configs {
|
||||||
@ -138,6 +140,7 @@ func (cfg *configsV1) mapToDatasourceFromConfig(apiVersion int64) *configs {
|
|||||||
SecureJSONData: ds.SecureJSONData.Value(),
|
SecureJSONData: ds.SecureJSONData.Value(),
|
||||||
Editable: ds.Editable.Value(),
|
Editable: ds.Editable.Value(),
|
||||||
Version: ds.Version.Value(),
|
Version: ds.Version.Value(),
|
||||||
|
UID: ds.UID.Value(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Using Raw value for the warnings here so that even if it uses env interpolation and the env var is empty
|
// Using Raw value for the warnings here so that even if it uses env interpolation and the env var is empty
|
||||||
@ -234,6 +237,7 @@ func createInsertCommand(ds *upsertDataSourceFromConfig) *models.AddDataSourceCo
|
|||||||
JsonData: jsonData,
|
JsonData: jsonData,
|
||||||
SecureJsonData: ds.SecureJSONData,
|
SecureJsonData: ds.SecureJSONData,
|
||||||
ReadOnly: !ds.Editable,
|
ReadOnly: !ds.Editable,
|
||||||
|
Uid: ds.UID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,6 +251,7 @@ func createUpdateCommand(ds *upsertDataSourceFromConfig, id int64) *models.Updat
|
|||||||
|
|
||||||
return &models.UpdateDataSourceCommand{
|
return &models.UpdateDataSourceCommand{
|
||||||
Id: id,
|
Id: id,
|
||||||
|
Uid: ds.UID,
|
||||||
OrgId: ds.OrgID,
|
OrgId: ds.OrgID,
|
||||||
Name: ds.Name,
|
Name: ds.Name,
|
||||||
Type: ds.Type,
|
Type: ds.Type,
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package sqlstore
|
package sqlstore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
@ -101,6 +103,14 @@ func AddDataSource(cmd *models.AddDataSourceCommand) error {
|
|||||||
cmd.JsonData = simplejson.New()
|
cmd.JsonData = simplejson.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.Uid == "" {
|
||||||
|
uid, err := generateNewDatasourceUid(sess, cmd.OrgId)
|
||||||
|
if err != nil {
|
||||||
|
return errutil.Wrapf(err, "Failed to generate UID for datasource %q", cmd.Name)
|
||||||
|
}
|
||||||
|
cmd.Uid = uid
|
||||||
|
}
|
||||||
|
|
||||||
ds := &models.DataSource{
|
ds := &models.DataSource{
|
||||||
OrgId: cmd.OrgId,
|
OrgId: cmd.OrgId,
|
||||||
Name: cmd.Name,
|
Name: cmd.Name,
|
||||||
@ -121,9 +131,13 @@ func AddDataSource(cmd *models.AddDataSourceCommand) error {
|
|||||||
Updated: time.Now(),
|
Updated: time.Now(),
|
||||||
Version: 1,
|
Version: 1,
|
||||||
ReadOnly: cmd.ReadOnly,
|
ReadOnly: cmd.ReadOnly,
|
||||||
|
Uid: cmd.Uid,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := sess.Insert(ds); err != nil {
|
if _, err := sess.Insert(ds); err != nil {
|
||||||
|
if dialect.IsUniqueConstraintViolation(err) && strings.Contains(strings.ToLower(dialect.ErrorMessage(err)), "uid") {
|
||||||
|
return models.ErrDataSourceUidExists
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := updateIsDefaultFlag(ds, sess); err != nil {
|
if err := updateIsDefaultFlag(ds, sess); err != nil {
|
||||||
@ -172,6 +186,7 @@ func UpdateDataSource(cmd *models.UpdateDataSourceCommand) error {
|
|||||||
Updated: time.Now(),
|
Updated: time.Now(),
|
||||||
ReadOnly: cmd.ReadOnly,
|
ReadOnly: cmd.ReadOnly,
|
||||||
Version: cmd.Version + 1,
|
Version: cmd.Version + 1,
|
||||||
|
Uid: cmd.Uid,
|
||||||
}
|
}
|
||||||
|
|
||||||
sess.UseBool("is_default")
|
sess.UseBool("is_default")
|
||||||
@ -209,3 +224,20 @@ func UpdateDataSource(cmd *models.UpdateDataSourceCommand) error {
|
|||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateNewDatasourceUid(sess *DBSession, orgId int64) (string, error) {
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
uid := generateNewUid()
|
||||||
|
|
||||||
|
exists, err := sess.Where("org_id=? AND uid=?", orgId, uid).Get(&models.DataSource{})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return uid, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", models.ErrDataSourceFailedGenerateUniqueUid
|
||||||
|
}
|
||||||
|
@ -3,173 +3,213 @@ package sqlstore
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Test struct {
|
|
||||||
Id int64
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDataAccess(t *testing.T) {
|
func TestDataAccess(t *testing.T) {
|
||||||
Convey("Testing DB", t, func() {
|
defaultAddDatasourceCommand := models.AddDataSourceCommand{
|
||||||
InitTestDB(t)
|
OrgId: 10,
|
||||||
Convey("Can add datasource", func() {
|
Name: "nisse",
|
||||||
|
Type: models.DS_GRAPHITE,
|
||||||
|
Access: models.DS_ACCESS_DIRECT,
|
||||||
|
Url: "http://test",
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultUpdateDatasourceCommand := models.UpdateDataSourceCommand{
|
||||||
|
OrgId: 10,
|
||||||
|
Name: "nisse_updated",
|
||||||
|
Type: models.DS_GRAPHITE,
|
||||||
|
Access: models.DS_ACCESS_DIRECT,
|
||||||
|
Url: "http://test",
|
||||||
|
}
|
||||||
|
|
||||||
|
initDatasource := func() *models.DataSource {
|
||||||
|
cmd := defaultAddDatasourceCommand
|
||||||
|
err := AddDataSource(&cmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
query := models.GetDataSourcesQuery{OrgId: 10}
|
||||||
|
err = GetDataSources(&query)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(query.Result))
|
||||||
|
|
||||||
|
return query.Result[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("AddDataSource", func(t *testing.T) {
|
||||||
|
t.Run("Can add datasource", func(t *testing.T) {
|
||||||
|
InitTestDB(t)
|
||||||
|
|
||||||
err := AddDataSource(&models.AddDataSourceCommand{
|
err := AddDataSource(&models.AddDataSourceCommand{
|
||||||
OrgId: 10,
|
OrgId: 10,
|
||||||
Name: "laban",
|
Name: "laban",
|
||||||
Type: models.DS_INFLUXDB,
|
Type: models.DS_GRAPHITE,
|
||||||
Access: models.DS_ACCESS_DIRECT,
|
Access: models.DS_ACCESS_DIRECT,
|
||||||
Url: "http://test",
|
Url: "http://test",
|
||||||
Database: "site",
|
Database: "site",
|
||||||
ReadOnly: true,
|
ReadOnly: true,
|
||||||
})
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
query := models.GetDataSourcesQuery{OrgId: 10}
|
query := models.GetDataSourcesQuery{OrgId: 10}
|
||||||
err = GetDataSources(&query)
|
err = GetDataSources(&query)
|
||||||
So(err, ShouldBeNil)
|
require.NoError(t, err)
|
||||||
|
|
||||||
So(len(query.Result), ShouldEqual, 1)
|
|
||||||
|
|
||||||
|
require.Equal(t, 1, len(query.Result))
|
||||||
ds := query.Result[0]
|
ds := query.Result[0]
|
||||||
|
|
||||||
So(ds.OrgId, ShouldEqual, 10)
|
require.EqualValues(t, 10, ds.OrgId)
|
||||||
So(ds.Database, ShouldEqual, "site")
|
require.Equal(t, "site", ds.Database)
|
||||||
So(ds.ReadOnly, ShouldBeTrue)
|
require.True(t, ds.ReadOnly)
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Given a datasource", func() {
|
t.Run("generates uid if not specified", func(t *testing.T) {
|
||||||
err := AddDataSource(&models.AddDataSourceCommand{
|
InitTestDB(t)
|
||||||
|
ds := initDatasource()
|
||||||
|
require.NotEmpty(t, ds.Uid)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fails to insert ds with same uid", func(t *testing.T) {
|
||||||
|
InitTestDB(t)
|
||||||
|
cmd1 := defaultAddDatasourceCommand
|
||||||
|
cmd2 := defaultAddDatasourceCommand
|
||||||
|
cmd1.Uid = "test"
|
||||||
|
cmd2.Uid = "test"
|
||||||
|
err := AddDataSource(&cmd1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = AddDataSource(&cmd2)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.IsType(t, models.ErrDataSourceUidExists, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateDataSource", func(t *testing.T) {
|
||||||
|
t.Run("updates datasource with version", func(t *testing.T) {
|
||||||
|
InitTestDB(t)
|
||||||
|
ds := initDatasource()
|
||||||
|
cmd := defaultUpdateDatasourceCommand
|
||||||
|
cmd.Id = ds.Id
|
||||||
|
cmd.Version = ds.Version
|
||||||
|
err := UpdateDataSource(&cmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("does not overwrite Uid if not specified", func(t *testing.T) {
|
||||||
|
InitTestDB(t)
|
||||||
|
ds := initDatasource()
|
||||||
|
require.NotEmpty(t, ds.Uid)
|
||||||
|
|
||||||
|
cmd := defaultUpdateDatasourceCommand
|
||||||
|
cmd.Id = ds.Id
|
||||||
|
err := UpdateDataSource(&cmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
query := models.GetDataSourceByIdQuery{Id: ds.Id}
|
||||||
|
err = GetDataSourceById(&query)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ds.Uid, query.Result.Uid)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("prevents update if version changed", func(t *testing.T) {
|
||||||
|
InitTestDB(t)
|
||||||
|
ds := initDatasource()
|
||||||
|
|
||||||
|
cmd := models.UpdateDataSourceCommand{
|
||||||
|
Id: ds.Id,
|
||||||
|
OrgId: 10,
|
||||||
|
Name: "nisse",
|
||||||
|
Type: models.DS_GRAPHITE,
|
||||||
|
Access: models.DS_ACCESS_PROXY,
|
||||||
|
Url: "http://test",
|
||||||
|
Version: ds.Version,
|
||||||
|
}
|
||||||
|
// Make a copy as UpdateDataSource modifies it
|
||||||
|
cmd2 := cmd
|
||||||
|
|
||||||
|
err := UpdateDataSource(&cmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = UpdateDataSource(&cmd2)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updates ds without version specified", func(t *testing.T) {
|
||||||
|
InitTestDB(t)
|
||||||
|
ds := initDatasource()
|
||||||
|
|
||||||
|
cmd := &models.UpdateDataSourceCommand{
|
||||||
|
Id: ds.Id,
|
||||||
OrgId: 10,
|
OrgId: 10,
|
||||||
Name: "nisse",
|
Name: "nisse",
|
||||||
Type: models.DS_GRAPHITE,
|
Type: models.DS_GRAPHITE,
|
||||||
Access: models.DS_ACCESS_DIRECT,
|
Access: models.DS_ACCESS_PROXY,
|
||||||
Url: "http://test",
|
Url: "http://test",
|
||||||
})
|
}
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
err := UpdateDataSource(cmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updates ds without higher version", func(t *testing.T) {
|
||||||
|
InitTestDB(t)
|
||||||
|
ds := initDatasource()
|
||||||
|
|
||||||
|
cmd := &models.UpdateDataSourceCommand{
|
||||||
|
Id: ds.Id,
|
||||||
|
OrgId: 10,
|
||||||
|
Name: "nisse",
|
||||||
|
Type: models.DS_GRAPHITE,
|
||||||
|
Access: models.DS_ACCESS_PROXY,
|
||||||
|
Url: "http://test",
|
||||||
|
Version: 90000,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := UpdateDataSource(cmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DeleteDataSourceById", func(t *testing.T) {
|
||||||
|
t.Run("can delete datasource", func(t *testing.T) {
|
||||||
|
InitTestDB(t)
|
||||||
|
ds := initDatasource()
|
||||||
|
|
||||||
|
err := DeleteDataSourceById(&models.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: ds.OrgId})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
query := models.GetDataSourcesQuery{OrgId: 10}
|
query := models.GetDataSourcesQuery{OrgId: 10}
|
||||||
err = GetDataSources(&query)
|
err = GetDataSources(&query)
|
||||||
So(err, ShouldBeNil)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ds := query.Result[0]
|
require.Equal(t, 0, len(query.Result))
|
||||||
|
})
|
||||||
|
|
||||||
Convey(" updated ", func() {
|
t.Run("Can not delete datasource with wrong orgId", func(t *testing.T) {
|
||||||
cmd := &models.UpdateDataSourceCommand{
|
InitTestDB(t)
|
||||||
Id: ds.Id,
|
ds := initDatasource()
|
||||||
OrgId: 10,
|
|
||||||
Name: "nisse",
|
|
||||||
Type: models.DS_GRAPHITE,
|
|
||||||
Access: models.DS_ACCESS_PROXY,
|
|
||||||
Url: "http://test",
|
|
||||||
Version: ds.Version,
|
|
||||||
}
|
|
||||||
|
|
||||||
Convey("with same version as source", func() {
|
err := DeleteDataSourceById(&models.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: 123123})
|
||||||
err := UpdateDataSource(cmd)
|
require.NoError(t, err)
|
||||||
So(err, ShouldBeNil)
|
query := models.GetDataSourcesQuery{OrgId: 10}
|
||||||
})
|
err = GetDataSources(&query)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
Convey("when someone else updated between read and update", func() {
|
require.Equal(t, 1, len(query.Result))
|
||||||
query := models.GetDataSourcesQuery{OrgId: 10}
|
|
||||||
err = GetDataSources(&query)
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
ds := query.Result[0]
|
|
||||||
intendedUpdate := &models.UpdateDataSourceCommand{
|
|
||||||
Id: ds.Id,
|
|
||||||
OrgId: 10,
|
|
||||||
Name: "nisse",
|
|
||||||
Type: models.DS_GRAPHITE,
|
|
||||||
Access: models.DS_ACCESS_PROXY,
|
|
||||||
Url: "http://test",
|
|
||||||
Version: ds.Version,
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFromOtherUser := &models.UpdateDataSourceCommand{
|
|
||||||
Id: ds.Id,
|
|
||||||
OrgId: 10,
|
|
||||||
Name: "nisse",
|
|
||||||
Type: models.DS_GRAPHITE,
|
|
||||||
Access: models.DS_ACCESS_PROXY,
|
|
||||||
Url: "http://test",
|
|
||||||
Version: ds.Version,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := UpdateDataSource(updateFromOtherUser)
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
err = UpdateDataSource(intendedUpdate)
|
|
||||||
So(err, ShouldNotBeNil)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("updating datasource without version", func() {
|
|
||||||
cmd := &models.UpdateDataSourceCommand{
|
|
||||||
Id: ds.Id,
|
|
||||||
OrgId: 10,
|
|
||||||
Name: "nisse",
|
|
||||||
Type: models.DS_GRAPHITE,
|
|
||||||
Access: models.DS_ACCESS_PROXY,
|
|
||||||
Url: "http://test",
|
|
||||||
}
|
|
||||||
|
|
||||||
Convey("should not raise errors", func() {
|
|
||||||
err := UpdateDataSource(cmd)
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("updating datasource without higher version", func() {
|
|
||||||
cmd := &models.UpdateDataSourceCommand{
|
|
||||||
Id: ds.Id,
|
|
||||||
OrgId: 10,
|
|
||||||
Name: "nisse",
|
|
||||||
Type: models.DS_GRAPHITE,
|
|
||||||
Access: models.DS_ACCESS_PROXY,
|
|
||||||
Url: "http://test",
|
|
||||||
Version: 90000,
|
|
||||||
}
|
|
||||||
|
|
||||||
Convey("should not raise errors", func() {
|
|
||||||
err := UpdateDataSource(cmd)
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Can delete datasource by id", func() {
|
|
||||||
err := DeleteDataSourceById(&models.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: ds.OrgId})
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
err = GetDataSources(&query)
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
So(len(query.Result), ShouldEqual, 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Can delete datasource by name", func() {
|
|
||||||
err := DeleteDataSourceByName(&models.DeleteDataSourceByNameCommand{Name: ds.Name, OrgId: ds.OrgId})
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
err = GetDataSources(&query)
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
So(len(query.Result), ShouldEqual, 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Can not delete datasource with wrong orgId", func() {
|
|
||||||
err := DeleteDataSourceById(&models.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: 123123})
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
err = GetDataSources(&query)
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
So(len(query.Result), ShouldEqual, 1)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("DeleteDataSourceByName", func(t *testing.T) {
|
||||||
|
InitTestDB(t)
|
||||||
|
ds := initDatasource()
|
||||||
|
query := models.GetDataSourcesQuery{OrgId: 10}
|
||||||
|
|
||||||
|
err := DeleteDataSourceByName(&models.DeleteDataSourceByNameCommand{Name: ds.Name, OrgId: ds.OrgId})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = GetDataSources(&query)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, 0, len(query.Result))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -133,4 +133,22 @@ func addDataSourceMigration(mg *Migrator) {
|
|||||||
|
|
||||||
const setEmptyJSONWhereNullJSON = `UPDATE data_source SET json_data = '{}' WHERE json_data is null`
|
const setEmptyJSONWhereNullJSON = `UPDATE data_source SET json_data = '{}' WHERE json_data is null`
|
||||||
mg.AddMigration("Update json_data with nulls", NewRawSqlMigration(setEmptyJSONWhereNullJSON))
|
mg.AddMigration("Update json_data with nulls", NewRawSqlMigration(setEmptyJSONWhereNullJSON))
|
||||||
|
|
||||||
|
// add column uid for linking
|
||||||
|
mg.AddMigration("Add uid column", NewAddColumnMigration(tableV2, &Column{
|
||||||
|
Name: "uid", Type: DB_NVarchar, Length: 40, Nullable: false, Default: "0",
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Initialize as id as that is unique already
|
||||||
|
mg.AddMigration(
|
||||||
|
"Update uid value",
|
||||||
|
NewRawSqlMigration("").
|
||||||
|
Sqlite("UPDATE data_source SET uid=printf('%09d',id);").
|
||||||
|
Postgres("UPDATE data_source SET uid=lpad('' || id::text,9,'0');").
|
||||||
|
Mysql("UPDATE data_source SET uid=lpad(id,9,'0');"),
|
||||||
|
)
|
||||||
|
|
||||||
|
mg.AddMigration("Add unique index datasource_org_id_uid", NewAddIndexMigration(tableV2, &Index{
|
||||||
|
Cols: []string{"org_id", "uid"}, Type: UniqueIndex,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ type Dialect interface {
|
|||||||
NoOpSql() string
|
NoOpSql() string
|
||||||
|
|
||||||
IsUniqueConstraintViolation(err error) bool
|
IsUniqueConstraintViolation(err error) bool
|
||||||
|
ErrorMessage(err error) string
|
||||||
IsDeadlock(err error) bool
|
IsDeadlock(err error) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,6 +148,13 @@ func (db *Mysql) IsUniqueConstraintViolation(err error) bool {
|
|||||||
return db.isThisError(err, mysqlerr.ER_DUP_ENTRY)
|
return db.isThisError(err, mysqlerr.ER_DUP_ENTRY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *Mysql) ErrorMessage(err error) string {
|
||||||
|
if driverErr, ok := err.(*mysql.MySQLError); ok {
|
||||||
|
return driverErr.Message
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (db *Mysql) IsDeadlock(err error) bool {
|
func (db *Mysql) IsDeadlock(err error) bool {
|
||||||
return db.isThisError(err, mysqlerr.ER_LOCK_DEADLOCK)
|
return db.isThisError(err, mysqlerr.ER_LOCK_DEADLOCK)
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,11 @@ package migrator
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/lib/pq"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/util/errutil"
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
"github.com/lib/pq"
|
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -149,6 +149,13 @@ func (db *Postgres) isThisError(err error, errcode string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *Postgres) ErrorMessage(err error) string {
|
||||||
|
if driverErr, ok := err.(*pq.Error); ok {
|
||||||
|
return driverErr.Message
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (db *Postgres) IsUniqueConstraintViolation(err error) bool {
|
func (db *Postgres) IsUniqueConstraintViolation(err error) bool {
|
||||||
return db.isThisError(err, "23505")
|
return db.isThisError(err, "23505")
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package migrator
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/mattn/go-sqlite3"
|
"github.com/mattn/go-sqlite3"
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
@ -95,6 +94,13 @@ func (db *Sqlite3) isThisError(err error, errcode int) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *Sqlite3) ErrorMessage(err error) string {
|
||||||
|
if driverErr, ok := err.(sqlite3.Error); ok {
|
||||||
|
return driverErr.Error()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (db *Sqlite3) IsUniqueConstraintViolation(err error) bool {
|
func (db *Sqlite3) IsUniqueConstraintViolation(err error) bool {
|
||||||
return db.isThisError(err, int(sqlite3.ErrConstraintUnique))
|
return db.isThisError(err, int(sqlite3.ErrConstraintUnique))
|
||||||
}
|
}
|
||||||
|
@ -101,11 +101,11 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
|||||||
getFieldLinks = (field: Field, rowIndex: number) => {
|
getFieldLinks = (field: Field, rowIndex: number) => {
|
||||||
const data = getLinksFromLogsField(field, rowIndex);
|
const data = getLinksFromLogsField(field, rowIndex);
|
||||||
return data.map(d => {
|
return data.map(d => {
|
||||||
if (d.link.meta?.datasourceName) {
|
if (d.link.meta?.datasourceUid) {
|
||||||
return {
|
return {
|
||||||
...d.linkModel,
|
...d.linkModel,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
this.props.splitOpen(d.link.meta.datasourceName, field.values.get(rowIndex));
|
this.props.splitOpen({ dataSourceUid: d.link.meta.datasourceUid, query: field.values.get(rowIndex) });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -121,14 +121,14 @@ export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<vo
|
|||||||
/**
|
/**
|
||||||
* Loads a new datasource identified by the given name.
|
* Loads a new datasource identified by the given name.
|
||||||
*/
|
*/
|
||||||
export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult<void> {
|
export function changeDatasource(exploreId: ExploreId, datasourceName: string): ThunkResult<void> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
let newDataSourceInstance: DataSourceApi;
|
let newDataSourceInstance: DataSourceApi;
|
||||||
|
|
||||||
if (!datasource) {
|
if (!datasourceName) {
|
||||||
newDataSourceInstance = await getDatasourceSrv().get();
|
newDataSourceInstance = await getDatasourceSrv().get();
|
||||||
} else {
|
} else {
|
||||||
newDataSourceInstance = await getDatasourceSrv().get(datasource);
|
newDataSourceInstance = await getDatasourceSrv().get(datasourceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
|
const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
|
||||||
@ -697,11 +697,12 @@ export function splitClose(itemId: ExploreId): ThunkResult<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the split view and copy the left state to be the right state.
|
* Open the split view and the right state is automatically initialized.
|
||||||
* The right state is automatically initialized.
|
* If options are specified it initializes that pane with the datasource and query from options.
|
||||||
* The copy keeps all query modifications but wipes the query results.
|
* Otherwise it copies the left state to be the right state. The copy keeps all query modifications but wipes the query
|
||||||
|
* results.
|
||||||
*/
|
*/
|
||||||
export function splitOpen(dataSourceName?: string, query?: string): ThunkResult<void> {
|
export function splitOpen(options?: { dataSourceUid: string; query: string }): ThunkResult<void> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
// Clone left state to become the right state
|
// Clone left state to become the right state
|
||||||
const leftState: ExploreItemState = getState().explore[ExploreId.left];
|
const leftState: ExploreItemState = getState().explore[ExploreId.left];
|
||||||
@ -710,19 +711,24 @@ export function splitOpen(dataSourceName?: string, query?: string): ThunkResult<
|
|||||||
};
|
};
|
||||||
const queryState = getState().location.query[ExploreId.left] as string;
|
const queryState = getState().location.query[ExploreId.left] as string;
|
||||||
const urlState = parseUrlState(queryState);
|
const urlState = parseUrlState(queryState);
|
||||||
|
|
||||||
|
// TODO: Instead of splitting and then setting query/datasource we may probably do it in one action call
|
||||||
rightState.queries = leftState.queries.slice();
|
rightState.queries = leftState.queries.slice();
|
||||||
rightState.urlState = urlState;
|
rightState.urlState = urlState;
|
||||||
dispatch(splitOpenAction({ itemState: rightState }));
|
dispatch(splitOpenAction({ itemState: rightState }));
|
||||||
|
|
||||||
if (dataSourceName && query) {
|
if (options) {
|
||||||
// This is hardcoded for Jaeger right now
|
// TODO: This is hardcoded for Jaeger right now. Need to be changed so that target datasource can define the
|
||||||
|
// query shape.
|
||||||
const queries = [
|
const queries = [
|
||||||
{
|
{
|
||||||
query,
|
query: options.query,
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
} as DataQuery,
|
} as DataQuery,
|
||||||
];
|
];
|
||||||
await dispatch(changeDatasource(ExploreId.right, dataSourceName));
|
|
||||||
|
const dataSourceSettings = getDatasourceSrv().getDataSourceSettingsByUid(options.dataSourceUid);
|
||||||
|
await dispatch(changeDatasource(ExploreId.right, dataSourceSettings.name));
|
||||||
await dispatch(setQueriesAction({ exploreId: ExploreId.right, queries }));
|
await dispatch(setQueriesAction({ exploreId: ExploreId.right, queries }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import config from 'app/core/config';
|
|||||||
import { importDataSourcePlugin } from './plugin_loader';
|
import { importDataSourcePlugin } from './plugin_loader';
|
||||||
import { DataSourceSrv as DataSourceService, getDataSourceSrv as getDataSourceService } from '@grafana/runtime';
|
import { DataSourceSrv as DataSourceService, getDataSourceSrv as getDataSourceService } from '@grafana/runtime';
|
||||||
// Types
|
// Types
|
||||||
import { AppEvents, DataSourceApi, DataSourceSelectItem, ScopedVars } from '@grafana/data';
|
import { AppEvents, DataSourceApi, DataSourceInstanceSettings, DataSourceSelectItem, ScopedVars } from '@grafana/data';
|
||||||
import { auto } from 'angular';
|
import { auto } from 'angular';
|
||||||
import { TemplateSrv } from '../templating/template_srv';
|
import { TemplateSrv } from '../templating/template_srv';
|
||||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||||
@ -30,6 +30,10 @@ export class DatasourceSrv implements DataSourceService {
|
|||||||
this.datasources = {};
|
this.datasources = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined {
|
||||||
|
return Object.values(config.datasources).find(ds => ds.uid === uid);
|
||||||
|
}
|
||||||
|
|
||||||
get(name?: string, scopedVars?: ScopedVars): Promise<DataSourceApi> {
|
get(name?: string, scopedVars?: ScopedVars): Promise<DataSourceApi> {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return this.get(config.defaultDatasource);
|
return this.get(config.defaultDatasource);
|
||||||
@ -93,12 +97,12 @@ export class DatasourceSrv implements DataSourceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll() {
|
getAll(): DataSourceInstanceSettings[] {
|
||||||
const { datasources } = config;
|
const { datasources } = config;
|
||||||
return Object.keys(datasources).map(name => datasources[name]);
|
return Object.keys(datasources).map(name => datasources[name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
getExternal() {
|
getExternal(): DataSourceInstanceSettings[] {
|
||||||
const datasources = this.getAll().filter(ds => !ds.meta.builtIn);
|
const datasources = this.getAll().filter(ds => !ds.meta.builtIn);
|
||||||
return sortBy(datasources, ['name']);
|
return sortBy(datasources, ['name']);
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ describe('datasource_srv', () => {
|
|||||||
config.datasources = {
|
config.datasources = {
|
||||||
buildInDs: {
|
buildInDs: {
|
||||||
id: 1,
|
id: 1,
|
||||||
|
uid: '1',
|
||||||
type: 'b',
|
type: 'b',
|
||||||
name: 'buildIn',
|
name: 'buildIn',
|
||||||
meta: { builtIn: true } as DataSourcePluginMeta,
|
meta: { builtIn: true } as DataSourcePluginMeta,
|
||||||
@ -31,6 +32,7 @@ describe('datasource_srv', () => {
|
|||||||
},
|
},
|
||||||
nonBuildIn: {
|
nonBuildIn: {
|
||||||
id: 2,
|
id: 2,
|
||||||
|
uid: '2',
|
||||||
type: 'e',
|
type: 'e',
|
||||||
name: 'external1',
|
name: 'external1',
|
||||||
meta: { builtIn: false } as DataSourcePluginMeta,
|
meta: { builtIn: false } as DataSourcePluginMeta,
|
||||||
@ -38,6 +40,7 @@ describe('datasource_srv', () => {
|
|||||||
},
|
},
|
||||||
nonExplore: {
|
nonExplore: {
|
||||||
id: 3,
|
id: 3,
|
||||||
|
uid: '3',
|
||||||
type: 'e2',
|
type: 'e2',
|
||||||
name: 'external2',
|
name: 'external2',
|
||||||
meta: {} as PluginMeta,
|
meta: {} as PluginMeta,
|
||||||
|
@ -57,6 +57,7 @@ async function withMockedBackendSrv(srv: BackendSrv, fn: () => Promise<void>) {
|
|||||||
|
|
||||||
const defaultSettings: DataSourceInstanceSettings = {
|
const defaultSettings: DataSourceInstanceSettings = {
|
||||||
id: 0,
|
id: 0,
|
||||||
|
uid: '0',
|
||||||
type: 'tracing',
|
type: 'tracing',
|
||||||
name: 'jaeger',
|
name: 'jaeger',
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -39,7 +39,7 @@ export const ConfigEditor = (props: Props) => {
|
|||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<MaxLinesField
|
<MaxLinesField
|
||||||
value={options.jsonData.maxLines}
|
value={options.jsonData.maxLines || ''}
|
||||||
onChange={value => onOptionsChange(setMaxLines(options, value))}
|
onChange={value => onOptionsChange(setMaxLines(options, value))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { DerivedField } from './DerivedField';
|
||||||
|
import DataSourcePicker from '../../../../core/components/Select/DataSourcePicker';
|
||||||
|
|
||||||
|
jest.mock('app/core/config', () => ({
|
||||||
|
config: {
|
||||||
|
featureToggles: {
|
||||||
|
tracingIntegration: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('app/features/plugins/datasource_srv', () => ({
|
||||||
|
getDatasourceSrv() {
|
||||||
|
return {
|
||||||
|
getExternal(): any[] {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('DerivedField', () => {
|
||||||
|
it('shows internal link if uid is set', () => {
|
||||||
|
const value = {
|
||||||
|
matcherRegex: '',
|
||||||
|
name: '',
|
||||||
|
datasourceUid: 'test',
|
||||||
|
};
|
||||||
|
const wrapper = shallow(<DerivedField value={value} onChange={() => {}} onDelete={() => {}} suggestions={[]} />);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('DataSourceSection')
|
||||||
|
.dive()
|
||||||
|
.find(DataSourcePicker).length
|
||||||
|
).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows url link if uid is not set', () => {
|
||||||
|
const value = {
|
||||||
|
matcherRegex: '',
|
||||||
|
name: '',
|
||||||
|
url: 'test',
|
||||||
|
};
|
||||||
|
const wrapper = shallow(<DerivedField value={value} onChange={() => {}} onDelete={() => {}} suggestions={[]} />);
|
||||||
|
expect(wrapper.find('DataSourceSection').length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { Button, FormField, DataLinkInput, stylesFactory, LegacyForms } from '@grafana/ui';
|
import { Button, FormField, DataLinkInput, stylesFactory, LegacyForms } from '@grafana/ui';
|
||||||
const { Switch } = LegacyForms;
|
const { Switch } = LegacyForms;
|
||||||
@ -9,6 +9,7 @@ import { DerivedFieldConfig } from '../types';
|
|||||||
import DataSourcePicker from 'app/core/components/Select/DataSourcePicker';
|
import DataSourcePicker from 'app/core/components/Select/DataSourcePicker';
|
||||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
|
import { usePrevious } from 'react-use';
|
||||||
|
|
||||||
const getStyles = stylesFactory(() => ({
|
const getStyles = stylesFactory(() => ({
|
||||||
row: css`
|
row: css`
|
||||||
@ -33,7 +34,18 @@ type Props = {
|
|||||||
export const DerivedField = (props: Props) => {
|
export const DerivedField = (props: Props) => {
|
||||||
const { value, onChange, onDelete, suggestions, className } = props;
|
const { value, onChange, onDelete, suggestions, className } = props;
|
||||||
const styles = getStyles();
|
const styles = getStyles();
|
||||||
const [hasIntenalLink, setHasInternalLink] = useState(!!value.datasourceName);
|
const [showInternalLink, setShowInternalLink] = useState(!!value.datasourceUid);
|
||||||
|
const previousUid = usePrevious(value.datasourceUid);
|
||||||
|
|
||||||
|
// Force internal link visibility change if uid changed outside of this component.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!previousUid && value.datasourceUid && !showInternalLink) {
|
||||||
|
setShowInternalLink(true);
|
||||||
|
}
|
||||||
|
if (previousUid && !value.datasourceUid && showInternalLink) {
|
||||||
|
setShowInternalLink(false);
|
||||||
|
}
|
||||||
|
}, [previousUid, value.datasourceUid, showInternalLink]);
|
||||||
|
|
||||||
const handleChange = (field: keyof typeof value) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (field: keyof typeof value) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
onChange({
|
onChange({
|
||||||
@ -81,11 +93,11 @@ export const DerivedField = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="URL"
|
label={showInternalLink ? 'Query' : 'URL'}
|
||||||
labelWidth={5}
|
labelWidth={5}
|
||||||
inputEl={
|
inputEl={
|
||||||
<DataLinkInput
|
<DataLinkInput
|
||||||
placeholder={'http://example.com/${__value.raw}'}
|
placeholder={showInternalLink ? '${__value.raw}' : 'http://example.com/${__value.raw}'}
|
||||||
value={value.url || ''}
|
value={value.url || ''}
|
||||||
onChange={newValue =>
|
onChange={newValue =>
|
||||||
onChange({
|
onChange({
|
||||||
@ -105,27 +117,27 @@ export const DerivedField = (props: Props) => {
|
|||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
<Switch
|
<Switch
|
||||||
label="Internal link"
|
label="Internal link"
|
||||||
checked={hasIntenalLink}
|
checked={showInternalLink}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
if (hasIntenalLink) {
|
if (showInternalLink) {
|
||||||
onChange({
|
onChange({
|
||||||
...value,
|
...value,
|
||||||
datasourceName: undefined,
|
datasourceUid: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setHasInternalLink(!hasIntenalLink);
|
setShowInternalLink(!showInternalLink);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasIntenalLink && (
|
{showInternalLink && (
|
||||||
<DataSourceSection
|
<DataSourceSection
|
||||||
onChange={datasourceName => {
|
onChange={datasourceUid => {
|
||||||
onChange({
|
onChange({
|
||||||
...value,
|
...value,
|
||||||
datasourceName,
|
datasourceUid,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
datasourceName={value.datasourceName}
|
datasourceUid={value.datasourceUid}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -135,29 +147,30 @@ export const DerivedField = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type DataSourceSectionProps = {
|
type DataSourceSectionProps = {
|
||||||
datasourceName?: string;
|
datasourceUid?: string;
|
||||||
onChange: (name: string) => void;
|
onChange: (uid: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DataSourceSection = (props: DataSourceSectionProps) => {
|
const DataSourceSection = (props: DataSourceSectionProps) => {
|
||||||
const { datasourceName, onChange } = props;
|
const { datasourceUid, onChange } = props;
|
||||||
const datasources: DataSourceSelectItem[] = getDatasourceSrv()
|
const datasources: DataSourceSelectItem[] = getDatasourceSrv()
|
||||||
.getExternal()
|
.getExternal()
|
||||||
.map(
|
.map(
|
||||||
(ds: any) =>
|
ds =>
|
||||||
({
|
({
|
||||||
value: ds.name,
|
value: ds.uid,
|
||||||
name: ds.name,
|
name: ds.name,
|
||||||
meta: ds.meta,
|
meta: ds.meta,
|
||||||
} as DataSourceSelectItem)
|
} as DataSourceSelectItem)
|
||||||
);
|
);
|
||||||
const selectedDatasource = datasourceName && datasources.find(d => d.name === datasourceName);
|
|
||||||
|
let selectedDatasource = datasourceUid && datasources.find(d => d.value === datasourceUid);
|
||||||
return (
|
return (
|
||||||
<DataSourcePicker
|
<DataSourcePicker
|
||||||
onChange={newValue => {
|
// Uid and value should be always set in the db and so in the items.
|
||||||
onChange(newValue.name);
|
onChange={ds => onChange(ds.value!)}
|
||||||
}}
|
|
||||||
datasources={datasources}
|
datasources={datasources}
|
||||||
current={selectedDatasource}
|
current={selectedDatasource || undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { CircularDataFrame, FieldType, MutableDataFrame } from '@grafana/data';
|
import { CircularDataFrame, FieldCache, FieldType, MutableDataFrame } from '@grafana/data';
|
||||||
import { LokiLegacyStreamResult, LokiStreamResult, LokiTailResponse } from './types';
|
import { LokiLegacyStreamResult, LokiStreamResult, LokiTailResponse } from './types';
|
||||||
import * as ResultTransformer from './result_transformer';
|
import * as ResultTransformer from './result_transformer';
|
||||||
|
import { enhanceDataFrame } from './result_transformer';
|
||||||
|
|
||||||
const legacyStreamResult: LokiLegacyStreamResult[] = [
|
const legacyStreamResult: LokiLegacyStreamResult[] = [
|
||||||
{
|
{
|
||||||
@ -180,3 +181,29 @@ describe('loki result transformer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('enhanceDataFrame', () => {
|
||||||
|
it('', () => {
|
||||||
|
const df = new MutableDataFrame({ fields: [{ name: 'line', values: ['nothing', 'trace1=1234', 'trace2=foo'] }] });
|
||||||
|
enhanceDataFrame(df, {
|
||||||
|
derivedFields: [
|
||||||
|
{
|
||||||
|
matcherRegex: 'trace1=(\\w+)',
|
||||||
|
name: 'trace1',
|
||||||
|
url: 'http://localhost/${__value.raw}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcherRegex: 'trace2=(\\w+)',
|
||||||
|
name: 'trace2',
|
||||||
|
datasourceUid: 'uid',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(df.fields.length).toBe(3);
|
||||||
|
const fc = new FieldCache(df);
|
||||||
|
expect(fc.getFieldByName('trace1').values.toArray()).toEqual([null, '1234', null]);
|
||||||
|
expect(fc.getFieldByName('trace1').config.links[0]).toEqual({ url: 'http://localhost/${__value.raw}', title: '' });
|
||||||
|
expect(fc.getFieldByName('trace2').values.toArray()).toEqual([null, null, 'foo']);
|
||||||
|
expect(fc.getFieldByName('trace2').config.links[0]).toEqual({ title: '', meta: { datasourceUid: 'uid' } });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -381,7 +381,6 @@ export function lokiLegacyStreamsToDataframes(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds new fields and DataLinks to DataFrame based on DataSource instance config.
|
* Adds new fields and DataLinks to DataFrame based on DataSource instance config.
|
||||||
* @param dataFrame
|
|
||||||
*/
|
*/
|
||||||
export const enhanceDataFrame = (dataFrame: DataFrame, config: LokiOptions | null): void => {
|
export const enhanceDataFrame = (dataFrame: DataFrame, config: LokiOptions | null): void => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
@ -395,14 +394,14 @@ export const enhanceDataFrame = (dataFrame: DataFrame, config: LokiOptions | nul
|
|||||||
|
|
||||||
const fields = derivedFields.reduce((acc, field) => {
|
const fields = derivedFields.reduce((acc, field) => {
|
||||||
const config: FieldConfig = {};
|
const config: FieldConfig = {};
|
||||||
if (field.url || field.datasourceName) {
|
if (field.url || field.datasourceUid) {
|
||||||
config.links = [
|
config.links = [
|
||||||
{
|
{
|
||||||
url: field.url,
|
url: field.url,
|
||||||
title: '',
|
title: '',
|
||||||
meta: field.datasourceName
|
meta: field.datasourceUid
|
||||||
? {
|
? {
|
||||||
datasourceName: field.datasourceName,
|
datasourceUid: field.datasourceUid,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
|
@ -128,7 +128,7 @@ export type DerivedFieldConfig = {
|
|||||||
matcherRegex: string;
|
matcherRegex: string;
|
||||||
name: string;
|
name: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
datasourceName?: string;
|
datasourceUid?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface TransformerOptions {
|
export interface TransformerOptions {
|
||||||
|
@ -44,7 +44,7 @@ export class QueryEditor extends PureComponent<Props> {
|
|||||||
onScenarioChange = (item: SelectableValue<string>) => {
|
onScenarioChange = (item: SelectableValue<string>) => {
|
||||||
this.props.onChange({
|
this.props.onChange({
|
||||||
...this.props.query,
|
...this.props.query,
|
||||||
scenarioId: item.value,
|
scenarioId: item.value!,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ export function runSignalStream(
|
|||||||
data.addField({ name: 'time', type: FieldType.time });
|
data.addField({ name: 'time', type: FieldType.time });
|
||||||
data.addField({ name: 'value', type: FieldType.number });
|
data.addField({ name: 'value', type: FieldType.number });
|
||||||
|
|
||||||
const { spread, speed, bands, noise } = query;
|
const { spread, speed, bands = 0, noise } = query;
|
||||||
|
|
||||||
for (let i = 0; i < bands; i++) {
|
for (let i = 0; i < bands; i++) {
|
||||||
const suffix = bands > 1 ? ` ${i + 1}` : '';
|
const suffix = bands > 1 ? ` ${i + 1}` : '';
|
||||||
@ -217,9 +217,15 @@ export function runFetchStream(
|
|||||||
return reader.read().then(processChunk);
|
return reader.read().then(processChunk);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!query.url) {
|
||||||
|
throw new Error('query.url is not defined');
|
||||||
|
}
|
||||||
|
|
||||||
fetch(new Request(query.url)).then(response => {
|
fetch(new Request(query.url)).then(response => {
|
||||||
reader = response.body.getReader();
|
if (response.body) {
|
||||||
reader.read().then(processChunk);
|
reader = response.body.getReader();
|
||||||
|
reader.read().then(processChunk);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -189,7 +189,7 @@ export class AnnoListPanel extends PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
renderTags = (tags: string[], remove: boolean): JSX.Element => {
|
renderTags = (tags: string[], remove: boolean): JSX.Element | null => {
|
||||||
if (!tags || !tags.length) {
|
if (!tags || !tags.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ echo -e "Collecting code stats (typescript errors & more)"
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
ERROR_COUNT_LIMIT=795
|
ERROR_COUNT_LIMIT=791
|
||||||
DIRECTIVES_LIMIT=172
|
DIRECTIVES_LIMIT=172
|
||||||
CONTROLLERS_LIMIT=139
|
CONTROLLERS_LIMIT=139
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user