mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Correlations: Allow correlations to target URLs (#92442)
* Pass one * Fix linter and add new betterer problem (sorry) * fix swagger * Add type to tests and update single correlations sql * Fix provisioning test and other function that needs a type * Add errors around query/external typing and add tests * increment number of correlations tested as we added one for testing v1 type placement * try merging back the swagger that is in main * try again? * Style form a little * Update public/app/features/logs/components/logParser.ts Co-authored-by: Matias Chomicki <matyax@gmail.com> * fix bad commit, simplify logic * Demonstrating type difficulties * Fix distributed union changes * Additional type changes * Update types in form * Fix swagger * Add comment around the assertion and explicit typing --------- Co-authored-by: Matias Chomicki <matyax@gmail.com> Co-authored-by: Andrej Ocenas <mr.ocenas@gmail.com>
This commit is contained in:
parent
8da1d78c92
commit
002f872ce1
@ -2631,6 +2631,11 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
[0, 0, 0, "Styles should be written using objects.", "1"]
|
||||||
],
|
],
|
||||||
|
"public/app/features/correlations/Forms/ConfigureCorrelationTargetForm.tsx:5381": [
|
||||||
|
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "2"]
|
||||||
|
],
|
||||||
"public/app/features/correlations/components/EmptyCorrelationsCTA.tsx:5381": [
|
"public/app/features/correlations/components/EmptyCorrelationsCTA.tsx:5381": [
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||||
@ -2639,6 +2644,10 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"],
|
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"],
|
||||||
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "1"]
|
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "1"]
|
||||||
],
|
],
|
||||||
|
"public/app/features/correlations/types.ts:5381": [
|
||||||
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||||
|
],
|
||||||
"public/app/features/dashboard-scene/embedding/EmbeddedDashboardTestPage.tsx:5381": [
|
"public/app/features/dashboard-scene/embedding/EmbeddedDashboardTestPage.tsx:5381": [
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||||
],
|
],
|
||||||
|
@ -51,6 +51,10 @@ export interface DataLink<T extends DataQuery = any> {
|
|||||||
internal?: InternalDataLink<T>;
|
internal?: InternalDataLink<T>;
|
||||||
|
|
||||||
origin?: DataLinkConfigOrigin;
|
origin?: DataLinkConfigOrigin;
|
||||||
|
meta?: {
|
||||||
|
correlationData?: ExploreCorrelationHelperData;
|
||||||
|
transformations?: DataLinkTransformationConfig[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,10 +82,6 @@ export interface InternalDataLink<T extends DataQuery = any> {
|
|||||||
datasourceUid: string;
|
datasourceUid: string;
|
||||||
datasourceName: string; // used as a title if `DataLink.title` is empty
|
datasourceName: string; // used as a title if `DataLink.title` is empty
|
||||||
panelsState?: ExplorePanelsState;
|
panelsState?: ExplorePanelsState;
|
||||||
meta?: {
|
|
||||||
correlationData?: ExploreCorrelationHelperData;
|
|
||||||
};
|
|
||||||
transformations?: DataLinkTransformationConfig[];
|
|
||||||
range?: TimeRange;
|
range?: TimeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,11 +40,7 @@ export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkMod
|
|||||||
|
|
||||||
const interpolatedQuery = interpolateObject(link.internal?.query, scopedVars, replaceVariables);
|
const interpolatedQuery = interpolateObject(link.internal?.query, scopedVars, replaceVariables);
|
||||||
const interpolatedPanelsState = interpolateObject(link.internal?.panelsState, scopedVars, replaceVariables);
|
const interpolatedPanelsState = interpolateObject(link.internal?.panelsState, scopedVars, replaceVariables);
|
||||||
const interpolatedCorrelationData = interpolateObject(
|
const interpolatedCorrelationData = interpolateObject(link.meta?.correlationData, scopedVars, replaceVariables);
|
||||||
link.internal?.meta?.correlationData,
|
|
||||||
scopedVars,
|
|
||||||
replaceVariables
|
|
||||||
);
|
|
||||||
const title = link.title ? link.title : internalLink.datasourceName;
|
const title = link.title ? link.title : internalLink.datasourceName;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -11,6 +11,8 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const VALID_TYPE_FILTER = "(correlation.type = 'external' OR (correlation.type = 'query' AND dst.uid IS NOT NULL))"
|
||||||
|
|
||||||
// createCorrelation adds a correlation
|
// createCorrelation adds a correlation
|
||||||
func (s CorrelationsService) createCorrelation(ctx context.Context, cmd CreateCorrelationCommand) (Correlation, error) {
|
func (s CorrelationsService) createCorrelation(ctx context.Context, cmd CreateCorrelationCommand) (Correlation, error) {
|
||||||
correlation := Correlation{
|
correlation := Correlation{
|
||||||
@ -25,6 +27,12 @@ func (s CorrelationsService) createCorrelation(ctx context.Context, cmd CreateCo
|
|||||||
Type: cmd.Type,
|
Type: cmd.Type,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if correlation.Config.Type == CorrelationType("query") {
|
||||||
|
correlation.Type = CorrelationType("query")
|
||||||
|
} else if correlation.Config.Type != "" {
|
||||||
|
return correlation, ErrInvalidConfigType
|
||||||
|
}
|
||||||
|
|
||||||
err := s.SQLStore.WithTransactionalDbSession(ctx, func(session *db.Session) error {
|
err := s.SQLStore.WithTransactionalDbSession(ctx, func(session *db.Session) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@ -184,7 +192,7 @@ func (s CorrelationsService) getCorrelation(ctx context.Context, cmd GetCorrelat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Correlations created before the fix #72498 may have org_id = 0, but it's deprecated and will be removed in #72325
|
// Correlations created before the fix #72498 may have org_id = 0, but it's deprecated and will be removed in #72325
|
||||||
found, err := session.Select("correlation.*").Join("", "data_source AS dss", "correlation.source_uid = dss.uid and (correlation.org_id = 0 or dss.org_id = correlation.org_id) and dss.org_id = ?", cmd.OrgId).Join("", "data_source AS dst", "correlation.target_uid = dst.uid and dst.org_id = ?", cmd.OrgId).Where("correlation.uid = ? AND correlation.source_uid = ?", correlation.UID, correlation.SourceUID).Get(&correlation)
|
found, err := session.Select("correlation.*").Join("", "data_source AS dss", "correlation.source_uid = dss.uid and (correlation.org_id = 0 or dss.org_id = correlation.org_id) and dss.org_id = ?", cmd.OrgId).Join("LEFT OUTER", "data_source AS dst", "correlation.target_uid = dst.uid and dst.org_id = ?", cmd.OrgId).Where("correlation.uid = ?", correlation.UID).And("correlation.source_uid = ?", correlation.SourceUID).And(VALID_TYPE_FILTER).Get(&correlation)
|
||||||
if !found {
|
if !found {
|
||||||
return ErrCorrelationNotFound
|
return ErrCorrelationNotFound
|
||||||
}
|
}
|
||||||
@ -235,7 +243,7 @@ func (s CorrelationsService) getCorrelationsBySourceUID(ctx context.Context, cmd
|
|||||||
return ErrSourceDataSourceDoesNotExists
|
return ErrSourceDataSourceDoesNotExists
|
||||||
}
|
}
|
||||||
// Correlations created before the fix #72498 may have org_id = 0, but it's deprecated and will be removed in #72325
|
// Correlations created before the fix #72498 may have org_id = 0, but it's deprecated and will be removed in #72325
|
||||||
return session.Select("correlation.*").Join("", "data_source AS dss", "correlation.source_uid = dss.uid and (correlation.org_id = 0 or dss.org_id = correlation.org_id) and dss.org_id = ?", cmd.OrgId).Join("", "data_source AS dst", "correlation.target_uid = dst.uid and dst.org_id = ?", cmd.OrgId).Where("correlation.source_uid = ?", cmd.SourceUID).Find(&correlations)
|
return session.Select("correlation.*").Join("", "data_source AS dss", "correlation.source_uid = dss.uid and (correlation.org_id = 0 or dss.org_id = correlation.org_id) and dss.org_id = ?", cmd.OrgId).Join("LEFT OUTER", "data_source AS dst", "correlation.target_uid = dst.uid and dst.org_id = ?", cmd.OrgId).Where("correlation.source_uid = ?", cmd.SourceUID).And(VALID_TYPE_FILTER).Find(&correlations)
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -256,12 +264,14 @@ func (s CorrelationsService) getCorrelations(ctx context.Context, cmd GetCorrela
|
|||||||
offset := cmd.Limit * (cmd.Page - 1)
|
offset := cmd.Limit * (cmd.Page - 1)
|
||||||
|
|
||||||
// Correlations created before the fix #72498 may have org_id = 0, but it's deprecated and will be removed in #72325
|
// Correlations created before the fix #72498 may have org_id = 0, but it's deprecated and will be removed in #72325
|
||||||
q := session.Select("correlation.*").Join("", "data_source AS dss", "correlation.source_uid = dss.uid and (correlation.org_id = 0 or dss.org_id = correlation.org_id) and dss.org_id = ? ", cmd.OrgId).Join("", "data_source AS dst", "correlation.target_uid = dst.uid and dst.org_id = ?", cmd.OrgId)
|
q := session.Select("correlation.*").Join("", "data_source AS dss", "correlation.source_uid = dss.uid and (correlation.org_id = 0 or dss.org_id = correlation.org_id) and dss.org_id = ? ", cmd.OrgId).Join("LEFT OUTER", "data_source AS dst", "correlation.target_uid = dst.uid and dst.org_id = ?", cmd.OrgId)
|
||||||
|
|
||||||
if len(cmd.SourceUIDs) > 0 {
|
if len(cmd.SourceUIDs) > 0 {
|
||||||
q.In("dss.uid", cmd.SourceUIDs)
|
q.In("dss.uid", cmd.SourceUIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
q.Where(VALID_TYPE_FILTER)
|
||||||
|
|
||||||
return q.Limit(int(cmd.Limit), int(offset)).Find(&result.Correlations)
|
return q.Limit(int(cmd.Limit), int(offset)).Find(&result.Correlations)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -316,6 +326,7 @@ func (s CorrelationsService) createOrUpdateCorrelation(ctx context.Context, cmd
|
|||||||
Description: cmd.Description,
|
Description: cmd.Description,
|
||||||
Config: cmd.Config,
|
Config: cmd.Config,
|
||||||
Provisioned: false,
|
Provisioned: false,
|
||||||
|
Type: cmd.Type,
|
||||||
}
|
}
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
|
@ -14,12 +14,13 @@ var (
|
|||||||
ErrTargetDataSourceDoesNotExists = errors.New("target data source does not exist")
|
ErrTargetDataSourceDoesNotExists = errors.New("target data source does not exist")
|
||||||
ErrCorrelationNotFound = errors.New("correlation not found")
|
ErrCorrelationNotFound = errors.New("correlation not found")
|
||||||
ErrUpdateCorrelationEmptyParams = errors.New("not enough parameters to edit correlation")
|
ErrUpdateCorrelationEmptyParams = errors.New("not enough parameters to edit correlation")
|
||||||
ErrInvalidConfigType = errors.New("invalid correlation config type")
|
ErrInvalidType = errors.New("invalid correlation type")
|
||||||
ErrInvalidTransformationType = errors.New("invalid transformation type")
|
ErrInvalidTransformationType = errors.New("invalid transformation type")
|
||||||
ErrTransformationNotNested = errors.New("transformations must be nested under config")
|
ErrTransformationNotNested = errors.New("transformations must be nested under config")
|
||||||
ErrTransformationRegexReqExp = errors.New("regex transformations require expression")
|
ErrTransformationRegexReqExp = errors.New("regex transformations require expression")
|
||||||
ErrCorrelationsQuotaFailed = errors.New("error getting correlations quota")
|
ErrCorrelationsQuotaFailed = errors.New("error getting correlations quota")
|
||||||
ErrCorrelationsQuotaReached = errors.New("correlations quota reached")
|
ErrCorrelationsQuotaReached = errors.New("correlations quota reached")
|
||||||
|
ErrInvalidConfigType = errors.New("correlation contains non default value in config.type")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -27,8 +28,15 @@ const (
|
|||||||
QuotaTarget quota.Target = "correlations"
|
QuotaTarget quota.Target = "correlations"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// the type of correlation, either query for containing query information, or external for containing an external URL
|
||||||
|
// +enum
|
||||||
type CorrelationType string
|
type CorrelationType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
query CorrelationType = "query"
|
||||||
|
external CorrelationType = "external"
|
||||||
|
)
|
||||||
|
|
||||||
type Transformation struct {
|
type Transformation struct {
|
||||||
//Enum: regex,logfmt
|
//Enum: regex,logfmt
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@ -37,13 +45,9 @@ type Transformation struct {
|
|||||||
MapValue string `json:"mapValue,omitempty"`
|
MapValue string `json:"mapValue,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
TypeQuery CorrelationType = "query"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (t CorrelationType) Validate() error {
|
func (t CorrelationType) Validate() error {
|
||||||
if t != TypeQuery {
|
if t != query && t != external {
|
||||||
return fmt.Errorf("%s: \"%s\"", ErrInvalidConfigType, t)
|
return fmt.Errorf("%s: \"%s\"", ErrInvalidType, t)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -161,7 +165,7 @@ type CreateCorrelationCommand struct {
|
|||||||
Config CorrelationConfig `json:"config" binding:"Required"`
|
Config CorrelationConfig `json:"config" binding:"Required"`
|
||||||
// True if correlation was created with provisioning. This makes it read-only.
|
// True if correlation was created with provisioning. This makes it read-only.
|
||||||
Provisioned bool `json:"provisioned"`
|
Provisioned bool `json:"provisioned"`
|
||||||
// correlation type, currently only valid value is "query"
|
// correlation type
|
||||||
Type CorrelationType `json:"type" binding:"Required"`
|
Type CorrelationType `json:"type" binding:"Required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,8 +173,8 @@ func (c CreateCorrelationCommand) Validate() error {
|
|||||||
if err := c.Type.Validate(); err != nil {
|
if err := c.Type.Validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if c.TargetUID == nil && c.Type == TypeQuery {
|
if c.TargetUID == nil && c.Type == query {
|
||||||
return fmt.Errorf("correlations of type \"%s\" must have a targetUID", TypeQuery)
|
return fmt.Errorf("correlations of type \"%s\" must have a targetUID", query)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.Config.Transformations.Validate(); err != nil {
|
if err := c.Config.Transformations.Validate(); err != nil {
|
||||||
|
@ -20,7 +20,7 @@ func TestCorrelationModels(t *testing.T) {
|
|||||||
OrgId: 1,
|
OrgId: 1,
|
||||||
TargetUID: &targetUid,
|
TargetUID: &targetUid,
|
||||||
Config: *config,
|
Config: *config,
|
||||||
Type: TypeQuery,
|
Type: query,
|
||||||
}
|
}
|
||||||
|
|
||||||
require.NoError(t, cmd.Validate())
|
require.NoError(t, cmd.Validate())
|
||||||
@ -30,7 +30,7 @@ func TestCorrelationModels(t *testing.T) {
|
|||||||
config := &CorrelationConfig{
|
config := &CorrelationConfig{
|
||||||
Field: "field",
|
Field: "field",
|
||||||
Target: map[string]any{},
|
Target: map[string]any{},
|
||||||
Type: TypeQuery,
|
Type: query,
|
||||||
}
|
}
|
||||||
cmd := &CreateCorrelationCommand{
|
cmd := &CreateCorrelationCommand{
|
||||||
SourceUID: "some-uid",
|
SourceUID: "some-uid",
|
||||||
|
@ -196,7 +196,7 @@ func makeCreateCorrelationCommand(correlation map[string]any, SourceUID string,
|
|||||||
// we ignore the legacy config.type value - the only valid value at that version was "query"
|
// we ignore the legacy config.type value - the only valid value at that version was "query"
|
||||||
var corrType = correlation["type"]
|
var corrType = correlation["type"]
|
||||||
if corrType == nil || corrType == "" {
|
if corrType == nil || corrType == "" {
|
||||||
corrType = correlations.TypeQuery
|
corrType = correlations.CorrelationType("query")
|
||||||
}
|
}
|
||||||
|
|
||||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
@ -229,6 +229,11 @@ func makeCreateCorrelationCommand(correlation map[string]any, SourceUID string,
|
|||||||
return correlations.CreateCorrelationCommand{}, err
|
return correlations.CreateCorrelationCommand{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// config.type is a deprecated place for this value. We will default it to "query" for legacy purposes but non-query correlations should have type outside of config
|
||||||
|
if config.Type != correlations.CorrelationType("query") {
|
||||||
|
return correlations.CreateCorrelationCommand{}, correlations.ErrInvalidConfigType
|
||||||
|
}
|
||||||
|
|
||||||
createCommand.Config = config
|
createCommand.Config = config
|
||||||
}
|
}
|
||||||
if err := createCommand.Validate(); err != nil {
|
if err := createCommand.Validate(); err != nil {
|
||||||
|
@ -104,7 +104,7 @@ func populateDB(t *testing.T, db db.DB, cfg *setting.Cfg) {
|
|||||||
Config: correlations.CorrelationConfig{
|
Config: correlations.CorrelationConfig{
|
||||||
Field: "field",
|
Field: "field",
|
||||||
Target: map[string]any{},
|
Target: map[string]any{},
|
||||||
Type: correlations.TypeQuery,
|
Type: correlations.CorrelationType("query"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
correlation, err := correlationsSvc.CreateCorrelation(context.Background(), cmd)
|
correlation, err := correlationsSvc.CreateCorrelation(context.Background(), cmd)
|
||||||
|
@ -193,6 +193,11 @@ func (c TestContext) createCorrelation(cmd correlations.CreateCorrelationCommand
|
|||||||
return correlation
|
return correlation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c TestContext) createCorrelationPassError(cmd correlations.CreateCorrelationCommand) (correlations.Correlation, error) {
|
||||||
|
c.t.Helper()
|
||||||
|
return c.env.Server.HTTPServer.CorrelationsService.CreateCorrelation(context.Background(), cmd)
|
||||||
|
}
|
||||||
|
|
||||||
func (c TestContext) createOrUpdateCorrelation(cmd correlations.CreateCorrelationCommand) {
|
func (c TestContext) createOrUpdateCorrelation(cmd correlations.CreateCorrelationCommand) {
|
||||||
c.t.Helper()
|
c.t.Helper()
|
||||||
err := c.env.Server.HTTPServer.CorrelationsService.CreateOrUpdateCorrelation(context.Background(), cmd)
|
err := c.env.Server.HTTPServer.CorrelationsService.CreateOrUpdateCorrelation(context.Background(), cmd)
|
||||||
|
@ -230,7 +230,7 @@ func TestIntegrationCreateCorrelation(t *testing.T) {
|
|||||||
description := "a description"
|
description := "a description"
|
||||||
label := "a label"
|
label := "a label"
|
||||||
fieldName := "fieldName"
|
fieldName := "fieldName"
|
||||||
corrType := correlations.TypeQuery
|
corrType := correlations.CorrelationType("query")
|
||||||
transformation := correlations.Transformation{Type: "logfmt"}
|
transformation := correlations.Transformation{Type: "logfmt"}
|
||||||
transformation2 := correlations.Transformation{Type: "regex", Expression: "testExpression", MapValue: "testVar"}
|
transformation2 := correlations.Transformation{Type: "regex", Expression: "testExpression", MapValue: "testVar"}
|
||||||
res := ctx.Post(PostParams{
|
res := ctx.Post(PostParams{
|
||||||
|
@ -135,6 +135,7 @@ func TestIntegrationDeleteCorrelation(t *testing.T) {
|
|||||||
TargetUID: &writableDs,
|
TargetUID: &writableDs,
|
||||||
OrgId: writableDsOrgId,
|
OrgId: writableDsOrgId,
|
||||||
Provisioned: true,
|
Provisioned: true,
|
||||||
|
Type: correlations.CorrelationType("query"),
|
||||||
})
|
})
|
||||||
|
|
||||||
res := ctx.Delete(DeleteParams{
|
res := ctx.Delete(DeleteParams{
|
||||||
@ -160,6 +161,7 @@ func TestIntegrationDeleteCorrelation(t *testing.T) {
|
|||||||
SourceUID: writableDs,
|
SourceUID: writableDs,
|
||||||
TargetUID: &writableDs,
|
TargetUID: &writableDs,
|
||||||
OrgId: writableDsOrgId,
|
OrgId: writableDsOrgId,
|
||||||
|
Type: correlations.CorrelationType("query"),
|
||||||
})
|
})
|
||||||
|
|
||||||
res := ctx.Delete(DeleteParams{
|
res := ctx.Delete(DeleteParams{
|
||||||
@ -192,6 +194,7 @@ func TestIntegrationDeleteCorrelation(t *testing.T) {
|
|||||||
SourceUID: writableDs,
|
SourceUID: writableDs,
|
||||||
TargetUID: &readOnlyDS,
|
TargetUID: &readOnlyDS,
|
||||||
OrgId: writableDsOrgId,
|
OrgId: writableDsOrgId,
|
||||||
|
Type: correlations.CorrelationType("query"),
|
||||||
})
|
})
|
||||||
|
|
||||||
res := ctx.Delete(DeleteParams{
|
res := ctx.Delete(DeleteParams{
|
||||||
@ -225,6 +228,7 @@ func TestIntegrationDeleteCorrelation(t *testing.T) {
|
|||||||
TargetUID: &readOnlyDS,
|
TargetUID: &readOnlyDS,
|
||||||
OrgId: writableDsOrgId,
|
OrgId: writableDsOrgId,
|
||||||
Provisioned: false,
|
Provisioned: false,
|
||||||
|
Type: correlations.CorrelationType("query"),
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx.createCorrelation(correlations.CreateCorrelationCommand{
|
ctx.createCorrelation(correlations.CreateCorrelationCommand{
|
||||||
@ -232,6 +236,7 @@ func TestIntegrationDeleteCorrelation(t *testing.T) {
|
|||||||
TargetUID: &readOnlyDS,
|
TargetUID: &readOnlyDS,
|
||||||
OrgId: writableDsOrgId,
|
OrgId: writableDsOrgId,
|
||||||
Provisioned: true,
|
Provisioned: true,
|
||||||
|
Type: correlations.CorrelationType("query"),
|
||||||
})
|
})
|
||||||
|
|
||||||
res := ctx.Delete(DeleteParams{
|
res := ctx.Delete(DeleteParams{
|
||||||
|
@ -2,6 +2,7 @@ package correlations
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
@ -38,8 +39,8 @@ func TestIntegrationCreateOrUpdateCorrelation(t *testing.T) {
|
|||||||
TargetUID: &dataSource.UID,
|
TargetUID: &dataSource.UID,
|
||||||
OrgId: dataSource.OrgID,
|
OrgId: dataSource.OrgID,
|
||||||
Label: "needs migration",
|
Label: "needs migration",
|
||||||
|
Type: correlations.CorrelationType("query"),
|
||||||
Config: correlations.CorrelationConfig{
|
Config: correlations.CorrelationConfig{
|
||||||
Type: correlations.TypeQuery,
|
|
||||||
Field: "foo",
|
Field: "foo",
|
||||||
Target: map[string]any{},
|
Target: map[string]any{},
|
||||||
Transformations: []correlations.Transformation{
|
Transformations: []correlations.Transformation{
|
||||||
@ -54,8 +55,8 @@ func TestIntegrationCreateOrUpdateCorrelation(t *testing.T) {
|
|||||||
TargetUID: &dataSource.UID,
|
TargetUID: &dataSource.UID,
|
||||||
OrgId: dataSource.OrgID,
|
OrgId: dataSource.OrgID,
|
||||||
Label: "existing",
|
Label: "existing",
|
||||||
|
Type: correlations.CorrelationType("query"),
|
||||||
Config: correlations.CorrelationConfig{
|
Config: correlations.CorrelationConfig{
|
||||||
Type: correlations.TypeQuery,
|
|
||||||
Field: "foo",
|
Field: "foo",
|
||||||
Target: map[string]any{},
|
Target: map[string]any{},
|
||||||
Transformations: []correlations.Transformation{
|
Transformations: []correlations.Transformation{
|
||||||
@ -65,6 +66,23 @@ func TestIntegrationCreateOrUpdateCorrelation(t *testing.T) {
|
|||||||
Provisioned: false,
|
Provisioned: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// v1 correlation where type is in config
|
||||||
|
v1Correlation := ctx.createCorrelation(correlations.CreateCorrelationCommand{
|
||||||
|
SourceUID: dataSource.UID,
|
||||||
|
TargetUID: &dataSource.UID,
|
||||||
|
OrgId: dataSource.OrgID,
|
||||||
|
Label: "v1 correlation",
|
||||||
|
Config: correlations.CorrelationConfig{
|
||||||
|
Type: correlations.CorrelationType("query"),
|
||||||
|
Field: "foo",
|
||||||
|
Target: map[string]any{},
|
||||||
|
Transformations: []correlations.Transformation{
|
||||||
|
{Type: "logfmt"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Provisioned: true,
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Correctly marks existing correlations as provisioned", func(t *testing.T) {
|
t.Run("Correctly marks existing correlations as provisioned", func(t *testing.T) {
|
||||||
// should be updated
|
// should be updated
|
||||||
ctx.createOrUpdateCorrelation(correlations.CreateCorrelationCommand{
|
ctx.createOrUpdateCorrelation(correlations.CreateCorrelationCommand{
|
||||||
@ -75,6 +93,7 @@ func TestIntegrationCreateOrUpdateCorrelation(t *testing.T) {
|
|||||||
Description: needsMigration.Description,
|
Description: needsMigration.Description,
|
||||||
Config: needsMigration.Config,
|
Config: needsMigration.Config,
|
||||||
Provisioned: true,
|
Provisioned: true,
|
||||||
|
Type: needsMigration.Type,
|
||||||
})
|
})
|
||||||
|
|
||||||
// should be added
|
// should be added
|
||||||
@ -86,6 +105,7 @@ func TestIntegrationCreateOrUpdateCorrelation(t *testing.T) {
|
|||||||
Description: needsMigration.Description,
|
Description: needsMigration.Description,
|
||||||
Config: needsMigration.Config,
|
Config: needsMigration.Config,
|
||||||
Provisioned: true,
|
Provisioned: true,
|
||||||
|
Type: needsMigration.Type,
|
||||||
})
|
})
|
||||||
|
|
||||||
res := ctx.Get(GetParams{
|
res := ctx.Get(GetParams{
|
||||||
@ -101,7 +121,7 @@ func TestIntegrationCreateOrUpdateCorrelation(t *testing.T) {
|
|||||||
err = json.Unmarshal(responseBody, &response)
|
err = json.Unmarshal(responseBody, &response)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Len(t, response.Correlations, 3)
|
require.Len(t, response.Correlations, 4)
|
||||||
|
|
||||||
unordered := make(map[string]correlations.Correlation)
|
unordered := make(map[string]correlations.Correlation)
|
||||||
for _, v := range response.Correlations {
|
for _, v := range response.Correlations {
|
||||||
@ -117,4 +137,44 @@ func TestIntegrationCreateOrUpdateCorrelation(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, res.Body.Close())
|
require.NoError(t, res.Body.Close())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("If Config.Type is query, provision without error but have value outside of config", func(t *testing.T) {
|
||||||
|
res := ctx.Get(GetParams{
|
||||||
|
url: fmt.Sprintf("/api/datasources/uid/%s/correlations/%s", dataSource.UID, v1Correlation.UID),
|
||||||
|
user: adminUser,
|
||||||
|
})
|
||||||
|
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||||
|
responseBody, err := io.ReadAll(res.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var response correlations.Correlation
|
||||||
|
err = json.Unmarshal(responseBody, &response)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.EqualValues(t, response.Config.Type, "")
|
||||||
|
require.EqualValues(t, v1Correlation.Config.Type, response.Type)
|
||||||
|
|
||||||
|
require.NoError(t, res.Body.Close())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("If Config.type is not query, throw an error", func(t *testing.T) {
|
||||||
|
_, err := ctx.createCorrelationPassError(correlations.CreateCorrelationCommand{
|
||||||
|
SourceUID: dataSource.UID,
|
||||||
|
TargetUID: &dataSource.UID,
|
||||||
|
OrgId: dataSource.OrgID,
|
||||||
|
Label: "bad v1 correlation",
|
||||||
|
Config: correlations.CorrelationConfig{
|
||||||
|
Type: correlations.CorrelationType("external"),
|
||||||
|
Field: "foo",
|
||||||
|
Target: map[string]any{},
|
||||||
|
Transformations: []correlations.Transformation{
|
||||||
|
{Type: "logfmt"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Provisioned: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorIs(t, err, correlations.ErrInvalidConfigType)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ func TestIntegrationReadCorrelation(t *testing.T) {
|
|||||||
SourceUID: dsWithCorrelations.UID,
|
SourceUID: dsWithCorrelations.UID,
|
||||||
TargetUID: &dsWithCorrelations.UID,
|
TargetUID: &dsWithCorrelations.UID,
|
||||||
OrgId: dsWithCorrelations.OrgID,
|
OrgId: dsWithCorrelations.OrgID,
|
||||||
Type: correlations.TypeQuery,
|
Type: correlations.CorrelationType("query"),
|
||||||
Config: correlations.CorrelationConfig{
|
Config: correlations.CorrelationConfig{
|
||||||
Field: "foo",
|
Field: "foo",
|
||||||
Target: map[string]any{},
|
Target: map[string]any{},
|
||||||
|
@ -128,6 +128,7 @@ func TestIntegrationUpdateCorrelation(t *testing.T) {
|
|||||||
TargetUID: &writableDs,
|
TargetUID: &writableDs,
|
||||||
OrgId: writableDsOrgId,
|
OrgId: writableDsOrgId,
|
||||||
Provisioned: true,
|
Provisioned: true,
|
||||||
|
Type: correlations.CorrelationType("query"),
|
||||||
})
|
})
|
||||||
|
|
||||||
res := ctx.Patch(PatchParams{
|
res := ctx.Patch(PatchParams{
|
||||||
@ -155,6 +156,7 @@ func TestIntegrationUpdateCorrelation(t *testing.T) {
|
|||||||
SourceUID: writableDs,
|
SourceUID: writableDs,
|
||||||
TargetUID: &writableDs,
|
TargetUID: &writableDs,
|
||||||
OrgId: writableDsOrgId,
|
OrgId: writableDsOrgId,
|
||||||
|
Type: correlations.CorrelationType("query"),
|
||||||
})
|
})
|
||||||
|
|
||||||
// no params
|
// no params
|
||||||
@ -220,6 +222,7 @@ func TestIntegrationUpdateCorrelation(t *testing.T) {
|
|||||||
TargetUID: &writableDs,
|
TargetUID: &writableDs,
|
||||||
OrgId: writableDsOrgId,
|
OrgId: writableDsOrgId,
|
||||||
Label: "a label",
|
Label: "a label",
|
||||||
|
Type: correlations.CorrelationType("query"),
|
||||||
})
|
})
|
||||||
|
|
||||||
res := ctx.Patch(PatchParams{
|
res := ctx.Patch(PatchParams{
|
||||||
@ -249,9 +252,9 @@ func TestIntegrationUpdateCorrelation(t *testing.T) {
|
|||||||
OrgId: writableDsOrgId,
|
OrgId: writableDsOrgId,
|
||||||
Label: "0",
|
Label: "0",
|
||||||
Description: "0",
|
Description: "0",
|
||||||
|
Type: correlations.CorrelationType("query"),
|
||||||
Config: correlations.CorrelationConfig{
|
Config: correlations.CorrelationConfig{
|
||||||
Field: "fieldName",
|
Field: "fieldName",
|
||||||
Type: "query",
|
|
||||||
Target: map[string]any{"expr": "foo"},
|
Target: map[string]any{"expr": "foo"},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -13793,6 +13793,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CorrelationType": {
|
"CorrelationType": {
|
||||||
|
"description": "the type of correlation, either query for containing query information, or external for containing an external URL\n+enum",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"CounterResetHint": {
|
"CounterResetHint": {
|
||||||
|
@ -23,7 +23,7 @@ import {
|
|||||||
createRemoveCorrelationResponse,
|
createRemoveCorrelationResponse,
|
||||||
createUpdateCorrelationResponse,
|
createUpdateCorrelationResponse,
|
||||||
} from './__mocks__/useCorrelations.mocks';
|
} from './__mocks__/useCorrelations.mocks';
|
||||||
import { Correlation, CreateCorrelationParams } from './types';
|
import { Correlation, CreateCorrelationParams, OmitUnion } from './types';
|
||||||
|
|
||||||
const renderWithContext = async (
|
const renderWithContext = async (
|
||||||
datasources: ConstructorParameters<typeof MockDataSourceSrv>[0] = {},
|
datasources: ConstructorParameters<typeof MockDataSourceSrv>[0] = {},
|
||||||
@ -43,7 +43,7 @@ const renderWithContext = async (
|
|||||||
|
|
||||||
throw createFetchCorrelationsError();
|
throw createFetchCorrelationsError();
|
||||||
},
|
},
|
||||||
post: async (url: string, data: Omit<CreateCorrelationParams, 'sourceUID'>) => {
|
post: async (url: string, data: OmitUnion<CreateCorrelationParams, 'sourceUID'>) => {
|
||||||
const matches = url.match(/^\/api\/datasources\/uid\/(?<sourceUID>[a-zA-Z0-9]+)\/correlations$/);
|
const matches = url.match(/^\/api\/datasources\/uid\/(?<sourceUID>[a-zA-Z0-9]+)\/correlations$/);
|
||||||
if (matches?.groups) {
|
if (matches?.groups) {
|
||||||
const { sourceUID } = matches.groups;
|
const { sourceUID } = matches.groups;
|
||||||
@ -54,7 +54,7 @@ const renderWithContext = async (
|
|||||||
|
|
||||||
throw createFetchCorrelationsError();
|
throw createFetchCorrelationsError();
|
||||||
},
|
},
|
||||||
patch: async (url: string, data: Omit<CreateCorrelationParams, 'sourceUID'>) => {
|
patch: async (url: string, data: OmitUnion<CreateCorrelationParams, 'sourceUID'>) => {
|
||||||
const matches = url.match(
|
const matches = url.match(
|
||||||
/^\/api\/datasources\/uid\/(?<sourceUID>[a-zA-Z0-9]+)\/correlations\/(?<correlationUid>[a-zA-Z0-9]+)$/
|
/^\/api\/datasources\/uid\/(?<sourceUID>[a-zA-Z0-9]+)\/correlations\/(?<correlationUid>[a-zA-Z0-9]+)$/
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
|||||||
import { negate } from 'lodash';
|
import { negate } from 'lodash';
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { isFetchError, reportInteraction } from '@grafana/runtime';
|
import { isFetchError, reportInteraction } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
@ -27,7 +27,7 @@ import { AccessControlAction } from 'app/types';
|
|||||||
import { AddCorrelationForm } from './Forms/AddCorrelationForm';
|
import { AddCorrelationForm } from './Forms/AddCorrelationForm';
|
||||||
import { EditCorrelationForm } from './Forms/EditCorrelationForm';
|
import { EditCorrelationForm } from './Forms/EditCorrelationForm';
|
||||||
import { EmptyCorrelationsCTA } from './components/EmptyCorrelationsCTA';
|
import { EmptyCorrelationsCTA } from './components/EmptyCorrelationsCTA';
|
||||||
import type { RemoveCorrelationParams } from './types';
|
import type { Correlation, RemoveCorrelationParams } from './types';
|
||||||
import { CorrelationData, useCorrelations } from './useCorrelations';
|
import { CorrelationData, useCorrelations } from './useCorrelations';
|
||||||
|
|
||||||
const sortDatasource: SortByFn<CorrelationData> = (a, b, column) =>
|
const sortDatasource: SortByFn<CorrelationData> = (a, b, column) =>
|
||||||
@ -238,7 +238,7 @@ interface ExpandedRowProps {
|
|||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
onUpdated: () => void;
|
onUpdated: () => void;
|
||||||
}
|
}
|
||||||
function ExpendedRow({ correlation: { source, target, ...correlation }, readOnly, onUpdated }: ExpandedRowProps) {
|
function ExpendedRow({ correlation: { source, ...correlation }, readOnly, onUpdated }: ExpandedRowProps) {
|
||||||
useEffect(
|
useEffect(
|
||||||
() => reportInteraction('grafana_correlations_details_expanded'),
|
() => reportInteraction('grafana_correlations_details_expanded'),
|
||||||
// we only want to fire this on first render
|
// we only want to fire this on first render
|
||||||
@ -246,13 +246,12 @@ function ExpendedRow({ correlation: { source, target, ...correlation }, readOnly
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
let corr: Correlation =
|
||||||
<EditCorrelationForm
|
correlation.type === 'query'
|
||||||
correlation={{ ...correlation, sourceUID: source.uid, targetUID: target.uid }}
|
? { ...correlation, type: 'query', sourceUID: source.uid, targetUID: correlation.target.uid }
|
||||||
onUpdated={onUpdated}
|
: { ...correlation, type: 'external', sourceUID: source.uid };
|
||||||
readOnly={readOnly}
|
|
||||||
/>
|
return <EditCorrelationForm correlation={corr} onUpdated={onUpdated} readOnly={readOnly} />;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDatasourceCellStyles = (theme: GrafanaTheme2) => ({
|
const getDatasourceCellStyles = (theme: GrafanaTheme2) => ({
|
||||||
@ -268,20 +267,22 @@ const getDatasourceCellStyles = (theme: GrafanaTheme2) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const DataSourceCell = memo(
|
const DataSourceCell = memo(
|
||||||
function DataSourceCell({
|
function DataSourceCell({ cell: { value } }: CellProps<CorrelationData, DataSourceInstanceSettings>) {
|
||||||
cell: { value },
|
|
||||||
}: CellProps<CorrelationData, CorrelationData['source'] | CorrelationData['target']>) {
|
|
||||||
const styles = useStyles2(getDatasourceCellStyles);
|
const styles = useStyles2(getDatasourceCellStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={styles.root}>
|
<span className={styles.root}>
|
||||||
<img src={value.meta.info.logos.small} alt="" className={styles.dsLogo} />
|
{value?.name !== undefined && (
|
||||||
{value.name}
|
<>
|
||||||
|
<img src={value.meta.info.logos.small} alt="" className={styles.dsLogo} />
|
||||||
|
{value.name}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
({ cell: { value } }, { cell: { value: prevValue } }) => {
|
({ cell: { value } }, { cell: { value: prevValue } }) => {
|
||||||
return value.type === prevValue.type && value.name === prevValue.name;
|
return value?.type === prevValue?.type && value?.name === prevValue?.name;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -24,6 +24,35 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getFormText = (queryType: string, dataSourceName?: string) => {
|
||||||
|
if (queryType === 'query') {
|
||||||
|
return {
|
||||||
|
title: t(
|
||||||
|
'correlations.source-form.query-title',
|
||||||
|
'Configure the data source that will link to {{dataSourceName}} (Step 3 of 3)',
|
||||||
|
{ dataSourceName }
|
||||||
|
),
|
||||||
|
descriptionPre: t(
|
||||||
|
'correlations.source-form.description-query-pre',
|
||||||
|
'You have used following variables in the target query:'
|
||||||
|
),
|
||||||
|
heading: t('correlations.source-form.heading-query', 'Variables used in the target query'),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
title: t(
|
||||||
|
'correlations.source-form.external-title',
|
||||||
|
'Configure the data source that will use the URL (Step 3 of 3)'
|
||||||
|
),
|
||||||
|
descriptionPre: t(
|
||||||
|
'correlations.source-form.description-external-pre',
|
||||||
|
'You have used following variables in the target URL:'
|
||||||
|
),
|
||||||
|
heading: t('correlations.source-form.heading-external', 'Variables used in the target URL'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const ConfigureCorrelationSourceForm = () => {
|
export const ConfigureCorrelationSourceForm = () => {
|
||||||
const { control, formState, register, getValues } = useFormContext<FormDTO>();
|
const { control, formState, register, getValues } = useFormContext<FormDTO>();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
@ -32,9 +61,13 @@ export const ConfigureCorrelationSourceForm = () => {
|
|||||||
const { correlation, readOnly } = useCorrelationsFormContext();
|
const { correlation, readOnly } = useCorrelationsFormContext();
|
||||||
|
|
||||||
const currentTargetQuery = getValues('config.target');
|
const currentTargetQuery = getValues('config.target');
|
||||||
|
const currentType = getValues('type');
|
||||||
const variables = getVariableUsageInfo(currentTargetQuery, {}).variables.map(
|
const variables = getVariableUsageInfo(currentTargetQuery, {}).variables.map(
|
||||||
(variable) => variable.variableName + (variable.fieldPath ? `.${variable.fieldPath}` : '')
|
(variable) => variable.variableName + (variable.fieldPath ? `.${variable.fieldPath}` : '')
|
||||||
);
|
);
|
||||||
|
const dataSourceName = getDatasourceSrv().getInstanceSettings(getValues('targetUID'))?.name;
|
||||||
|
|
||||||
|
const formText = getFormText(currentType, dataSourceName);
|
||||||
|
|
||||||
function VariableList() {
|
function VariableList() {
|
||||||
return (
|
return (
|
||||||
@ -49,16 +82,9 @@ export const ConfigureCorrelationSourceForm = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataSourceName = getDatasourceSrv().getInstanceSettings(getValues('targetUID'))?.name;
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FieldSet
|
<FieldSet label={formText.title}>
|
||||||
label={t(
|
|
||||||
'correlations.source-form.title',
|
|
||||||
'Configure the data source that will link to {{dataSourceName}} (Step 3 of 3)',
|
|
||||||
{ dataSourceName }
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Trans i18nKey="correlations.source-form.sub-text">
|
<Trans i18nKey="correlations.source-form.sub-text">
|
||||||
<p>
|
<p>
|
||||||
Define what data source will display the correlation, and what data will replace previously defined
|
Define what data source will display the correlation, and what data will replace previously defined
|
||||||
@ -117,14 +143,14 @@ export const ConfigureCorrelationSourceForm = () => {
|
|||||||
</Field>
|
</Field>
|
||||||
{variables.length > 0 && (
|
{variables.length > 0 && (
|
||||||
<Card>
|
<Card>
|
||||||
<Card.Heading>
|
<Card.Heading>{formText.heading}</Card.Heading>
|
||||||
<Trans i18nKey="correlations.source-form.heading">Variables used in the target query</Trans>
|
|
||||||
</Card.Heading>
|
|
||||||
<Card.Description>
|
<Card.Description>
|
||||||
|
{formText.descriptionPre}
|
||||||
|
<VariableList />
|
||||||
|
<br />
|
||||||
<Trans i18nKey="correlations.source-form.description">
|
<Trans i18nKey="correlations.source-form.description">
|
||||||
You have used following variables in the target query: <VariableList />
|
A data point needs to provide values to all variables as fields or as transformations output to make the
|
||||||
<br />A data point needs to provide values to all variables as fields or as transformations output to
|
correlation button appear in the visualization.
|
||||||
make the correlation button appear in the visualization.
|
|
||||||
<br />
|
<br />
|
||||||
Note: Not every variable needs to be explicitly defined below. A transformation such as{' '}
|
Note: Not every variable needs to be explicitly defined below. A transformation such as{' '}
|
||||||
<span className={styles.variable}>logfmt</span> will create variables for every key/value pair.
|
<span className={styles.variable}>logfmt</span> will create variables for every key/value pair.
|
||||||
|
@ -1,64 +1,173 @@
|
|||||||
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
import { css } from '@emotion/css';
|
||||||
|
import { Controller, FieldError, useFormContext, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Field, FieldSet } from '@grafana/ui';
|
import { Field, FieldSet, Input, Select, useStyles2 } from '@grafana/ui';
|
||||||
import { Trans, t } from 'app/core/internationalization';
|
import { Trans, t } from 'app/core/internationalization';
|
||||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||||
|
|
||||||
|
import { CorrelationType, ExternalTypeTarget } from '../types';
|
||||||
|
|
||||||
import { QueryEditorField } from './QueryEditorField';
|
import { QueryEditorField } from './QueryEditorField';
|
||||||
import { useCorrelationsFormContext } from './correlationsFormContext';
|
import { useCorrelationsFormContext } from './correlationsFormContext';
|
||||||
import { FormDTO } from './types';
|
import { assertIsQueryTypeError, FormDTO } from './types';
|
||||||
|
|
||||||
|
type CorrelationTypeOptions = {
|
||||||
|
value: CorrelationType;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CORR_TYPES_SELECT: Record<CorrelationType, CorrelationTypeOptions> = {
|
||||||
|
query: {
|
||||||
|
value: 'query',
|
||||||
|
label: 'Query',
|
||||||
|
description: 'Open a query',
|
||||||
|
},
|
||||||
|
external: {
|
||||||
|
value: 'external',
|
||||||
|
label: 'External',
|
||||||
|
description: 'Open an external URL',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
typeSelect: css`
|
||||||
|
max-width: ${theme.spacing(40)};
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
export const ConfigureCorrelationTargetForm = () => {
|
export const ConfigureCorrelationTargetForm = () => {
|
||||||
const { control, formState } = useFormContext<FormDTO>();
|
const {
|
||||||
|
control,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext<FormDTO>();
|
||||||
const withDsUID = (fn: Function) => (ds: DataSourceInstanceSettings) => fn(ds.uid);
|
const withDsUID = (fn: Function) => (ds: DataSourceInstanceSettings) => fn(ds.uid);
|
||||||
const { correlation } = useCorrelationsFormContext();
|
const { correlation } = useCorrelationsFormContext();
|
||||||
const targetUID: string | undefined = useWatch({ name: 'targetUID' }) || correlation?.targetUID;
|
const targetUIDFromCorrelation = correlation && 'targetUID' in correlation ? correlation.targetUID : undefined;
|
||||||
|
const targetUID: string | undefined = useWatch({ name: 'targetUID' }) || targetUIDFromCorrelation;
|
||||||
|
const correlationType: CorrelationType | undefined = useWatch({ name: 'type' }) || correlation?.type;
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FieldSet label={t('correlations.target-form.title', 'Setup the target for the correlation (Step 2 of 3)')}>
|
<FieldSet label={t('correlations.target-form.title', 'Setup the target for the correlation (Step 2 of 3)')}>
|
||||||
<Trans i18nKey="correlations.target-form.sub-text">
|
<Trans i18nKey="correlations.target-form.sub-text">
|
||||||
<p>
|
<p>
|
||||||
Define what data source the correlation will link to, and what query will run when the correlation is
|
Define what the correlation will link to. With the query type, a query will run when the correlation is
|
||||||
clicked.
|
clicked. With the external type, clicking the correlation will open a URL.
|
||||||
</p>
|
</p>
|
||||||
</Trans>
|
</Trans>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="targetUID"
|
name="type"
|
||||||
rules={{
|
rules={{
|
||||||
required: { value: true, message: t('correlations.target-form.control-rules', 'This field is required.') },
|
required: { value: true, message: t('correlations.target-form.control-rules', 'This field is required.') },
|
||||||
}}
|
}}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value, ...field } }) => (
|
||||||
<Field
|
<Field
|
||||||
label={t('correlations.target-form.target-label', 'Target')}
|
label={t('correlations.target-form.type-label', 'Type')}
|
||||||
description={t(
|
description={t('correlations.target-form.target-type-description', 'Specify the type of correlation')}
|
||||||
'correlations.target-form.target-description',
|
htmlFor="corrType"
|
||||||
'Specify which data source is queried when the link is clicked'
|
invalid={!!errors.type}
|
||||||
)}
|
|
||||||
htmlFor="target"
|
|
||||||
invalid={!!formState.errors.targetUID}
|
|
||||||
error={formState.errors.targetUID?.message}
|
|
||||||
>
|
>
|
||||||
<DataSourcePicker
|
<Select
|
||||||
onChange={withDsUID(onChange)}
|
className={styles.typeSelect}
|
||||||
noDefault
|
value={correlationType}
|
||||||
current={value}
|
onChange={(value) => onChange(value.value)}
|
||||||
inputId="target"
|
options={Object.values(CORR_TYPES_SELECT)}
|
||||||
width={32}
|
aria-label="correlation type"
|
||||||
disabled={correlation !== undefined}
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<QueryEditorField
|
{correlationType === 'query' &&
|
||||||
name="config.target"
|
(() => {
|
||||||
dsUid={targetUID}
|
assertIsQueryTypeError(errors);
|
||||||
invalid={!!formState.errors?.config?.target}
|
// the assert above will make sure the form dto, which can be either external or query, is for query
|
||||||
error={formState.errors?.config?.target?.message}
|
// however, the query type has config.target, an object, which doesn't get converted, so we must explicity type it below
|
||||||
/>
|
return (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetUID"
|
||||||
|
rules={{
|
||||||
|
required: {
|
||||||
|
value: true,
|
||||||
|
message: t('correlations.target-form.control-rules', 'This field is required.'),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Field
|
||||||
|
label={t('correlations.target-form.target-label', 'Target')}
|
||||||
|
description={t(
|
||||||
|
'correlations.target-form.target-description-query',
|
||||||
|
'Specify which data source is queried when the link is clicked'
|
||||||
|
)}
|
||||||
|
htmlFor="target"
|
||||||
|
invalid={!!errors.targetUID}
|
||||||
|
error={errors.targetUID?.message}
|
||||||
|
>
|
||||||
|
<DataSourcePicker
|
||||||
|
onChange={withDsUID(onChange)}
|
||||||
|
noDefault
|
||||||
|
current={value}
|
||||||
|
inputId="target"
|
||||||
|
width={32}
|
||||||
|
disabled={correlation !== undefined}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<QueryEditorField
|
||||||
|
name="config.target"
|
||||||
|
dsUid={targetUID}
|
||||||
|
invalid={!!errors?.config?.target}
|
||||||
|
error={
|
||||||
|
errors?.config?.target && 'message' in errors?.config?.target
|
||||||
|
? (errors?.config?.target as FieldError).message
|
||||||
|
: 'Error'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
{correlationType === 'external' && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="config.target"
|
||||||
|
rules={{
|
||||||
|
required: {
|
||||||
|
value: true,
|
||||||
|
message: t('correlations.target-form.control-rules', 'This field is required.'),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
render={({ field: { onChange, value } }) => {
|
||||||
|
const castVal = value as ExternalTypeTarget; // the target under "query" type can contain anything a datasource query contains
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
label={t('correlations.target-form.target-label', 'Target')}
|
||||||
|
description={t(
|
||||||
|
'correlations.target-form.target-description-external',
|
||||||
|
'Specify the URL that will open when the link is clicked'
|
||||||
|
)}
|
||||||
|
htmlFor="target"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={castVal.url || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange({ url: e.currentTarget.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -52,12 +52,14 @@ export const QueryEditorField = ({ dsUid, invalid, error, name }: Props) => {
|
|||||||
name={name}
|
name={name}
|
||||||
rules={{
|
rules={{
|
||||||
validate: {
|
validate: {
|
||||||
hasQueryEditor: () =>
|
hasQueryEditor: (_, formVals) => {
|
||||||
QueryEditor !== undefined ||
|
return formVals.type === 'query' && QueryEditor === undefined
|
||||||
t(
|
? t(
|
||||||
'correlations.query-editor.control-rules',
|
'correlations.query-editor.control-rules',
|
||||||
'The selected target data source must export a query editor.'
|
'The selected target data source must export a query editor.'
|
||||||
),
|
)
|
||||||
|
: true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
render={({ field: { value, onChange } }) => {
|
render={({ field: { value, onChange } }) => {
|
||||||
|
@ -1,18 +1,36 @@
|
|||||||
|
import { DeepMap, FieldError, FieldErrors } from 'react-hook-form';
|
||||||
|
|
||||||
import { SupportedTransformationType } from '@grafana/data';
|
import { SupportedTransformationType } from '@grafana/data';
|
||||||
import { t } from 'app/core/internationalization';
|
import { t } from 'app/core/internationalization';
|
||||||
|
|
||||||
import { CorrelationConfig, CorrelationType } from '../types';
|
import { CorrelationConfigExternal, CorrelationConfigQuery, OmitUnion } from '../types';
|
||||||
|
|
||||||
export interface FormDTO {
|
export interface FormExternalDTO {
|
||||||
|
sourceUID: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
type: 'external';
|
||||||
|
config: CorrelationConfigExternal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormQueryDTO {
|
||||||
sourceUID: string;
|
sourceUID: string;
|
||||||
targetUID: string;
|
targetUID: string;
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
type: CorrelationType;
|
type: 'query';
|
||||||
config: CorrelationConfig;
|
config: CorrelationConfigQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EditFormDTO = Omit<FormDTO, 'targetUID' | 'sourceUID'>;
|
export type FormDTO = FormExternalDTO | FormQueryDTO;
|
||||||
|
|
||||||
|
export function assertIsQueryTypeError(
|
||||||
|
errors: FieldErrors<FormDTO>
|
||||||
|
): asserts errors is DeepMap<FormQueryDTO, FieldError> {
|
||||||
|
// explicitly assert the type so that TS can narrow down FormDTO to FormQueryDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EditFormDTO = OmitUnion<FormDTO, 'targetUID' | 'sourceUID'>;
|
||||||
|
|
||||||
export type TransformationDTO = {
|
export type TransformationDTO = {
|
||||||
type: SupportedTransformationType;
|
type: SupportedTransformationType;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Correlation } from '../types';
|
import { Correlation } from '../types';
|
||||||
|
|
||||||
type CorrelationBaseData = Pick<Correlation, 'uid' | 'sourceUID' | 'targetUID'>;
|
type CorrelationBaseData = Pick<Correlation, 'uid' | 'sourceUID'>;
|
||||||
|
|
||||||
export const getInputId = (inputName: string, correlation?: CorrelationBaseData) => {
|
export const getInputId = (inputName: string, correlation?: CorrelationBaseData) => {
|
||||||
if (!correlation) {
|
if (!correlation) {
|
||||||
|
@ -26,30 +26,50 @@ export interface RemoveCorrelationResponse {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CorrelationType = 'query';
|
export type CorrelationType = 'query' | 'external';
|
||||||
|
|
||||||
export interface CorrelationConfig {
|
export type ExternalTypeTarget = { url: string };
|
||||||
|
|
||||||
|
export type CorrelationConfigQuery = {
|
||||||
field: string;
|
field: string;
|
||||||
target: object; // this contains anything that would go in the query editor, so any extension off DataQuery a datasource would have, and needs to be generic
|
target: object; // for queries, this contains anything that would go in the query editor, so any extension off DataQuery a datasource would have, and needs to be generic.
|
||||||
transformations?: DataLinkTransformationConfig[];
|
transformations?: DataLinkTransformationConfig[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface Correlation {
|
export type CorrelationConfigExternal = {
|
||||||
|
field: string;
|
||||||
|
target: ExternalTypeTarget; // For external, this simply contains a URL
|
||||||
|
transformations?: DataLinkTransformationConfig[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type CorrelationBase = {
|
||||||
uid: string;
|
uid: string;
|
||||||
sourceUID: string;
|
sourceUID: string;
|
||||||
targetUID: string;
|
|
||||||
label?: string;
|
label?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
provisioned: boolean;
|
provisioned: boolean;
|
||||||
orgId?: number;
|
orgId?: number;
|
||||||
config: CorrelationConfig;
|
};
|
||||||
type: CorrelationType;
|
|
||||||
}
|
export type CorrelationExternal = CorrelationBase & {
|
||||||
|
type: 'external';
|
||||||
|
config: CorrelationConfigExternal;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CorrelationQuery = CorrelationBase & {
|
||||||
|
type: 'query';
|
||||||
|
config: CorrelationConfigQuery;
|
||||||
|
targetUID: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Correlation = CorrelationExternal | CorrelationQuery;
|
||||||
|
|
||||||
export type GetCorrelationsParams = {
|
export type GetCorrelationsParams = {
|
||||||
page: number;
|
page: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type OmitUnion<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
|
||||||
|
|
||||||
export type RemoveCorrelationParams = Pick<Correlation, 'sourceUID' | 'uid'>;
|
export type RemoveCorrelationParams = Pick<Correlation, 'sourceUID' | 'uid'>;
|
||||||
export type CreateCorrelationParams = Omit<Correlation, 'uid' | 'provisioned'>;
|
export type CreateCorrelationParams = OmitUnion<Correlation, 'uid' | 'provisioned'>;
|
||||||
export type UpdateCorrelationParams = Omit<Correlation, 'targetUID' | 'provisioned'>;
|
export type UpdateCorrelationParams = OmitUnion<Correlation, 'targetUID' | 'provisioned'>;
|
||||||
|
@ -7,6 +7,8 @@ import { useGrafana } from 'app/core/context/GrafanaContext';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Correlation,
|
Correlation,
|
||||||
|
CorrelationExternal,
|
||||||
|
CorrelationQuery,
|
||||||
CreateCorrelationParams,
|
CreateCorrelationParams,
|
||||||
CreateCorrelationResponse,
|
CreateCorrelationResponse,
|
||||||
GetCorrelationsParams,
|
GetCorrelationsParams,
|
||||||
@ -24,10 +26,14 @@ export interface CorrelationsResponse {
|
|||||||
totalCount: number;
|
totalCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CorrelationData extends Omit<Correlation, 'sourceUID' | 'targetUID'> {
|
export type CorrelationData =
|
||||||
source: DataSourceInstanceSettings;
|
| (Omit<CorrelationExternal, 'sourceUID'> & {
|
||||||
target: DataSourceInstanceSettings;
|
source: DataSourceInstanceSettings;
|
||||||
}
|
})
|
||||||
|
| (Omit<CorrelationQuery, 'sourceUID' | 'targetUID'> & {
|
||||||
|
source: DataSourceInstanceSettings;
|
||||||
|
target: DataSourceInstanceSettings;
|
||||||
|
});
|
||||||
|
|
||||||
export interface CorrelationsData {
|
export interface CorrelationsData {
|
||||||
correlations: CorrelationData[];
|
correlations: CorrelationData[];
|
||||||
@ -36,13 +42,10 @@ export interface CorrelationsData {
|
|||||||
totalCount: number;
|
totalCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toEnrichedCorrelationData = ({
|
const toEnrichedCorrelationData = ({ sourceUID, ...correlation }: Correlation): CorrelationData | undefined => {
|
||||||
sourceUID,
|
|
||||||
targetUID,
|
|
||||||
...correlation
|
|
||||||
}: Correlation): CorrelationData | undefined => {
|
|
||||||
const sourceDatasource = getDataSourceSrv().getInstanceSettings(sourceUID);
|
const sourceDatasource = getDataSourceSrv().getInstanceSettings(sourceUID);
|
||||||
const targetDatasource = getDataSourceSrv().getInstanceSettings(targetUID);
|
const targetDatasource =
|
||||||
|
correlation.type === 'query' ? getDataSourceSrv().getInstanceSettings(correlation.targetUID) : undefined;
|
||||||
|
|
||||||
// According to #72258 we will remove logic to handle orgId=0/null as global correlations.
|
// According to #72258 we will remove logic to handle orgId=0/null as global correlations.
|
||||||
// This logging is to check if there are any customers who did not migrate existing correlations.
|
// This logging is to check if there are any customers who did not migrate existing correlations.
|
||||||
@ -54,21 +57,33 @@ const toEnrichedCorrelationData = ({
|
|||||||
if (
|
if (
|
||||||
sourceDatasource &&
|
sourceDatasource &&
|
||||||
sourceDatasource?.uid !== undefined &&
|
sourceDatasource?.uid !== undefined &&
|
||||||
targetDatasource &&
|
targetDatasource?.uid !== undefined &&
|
||||||
targetDatasource.uid !== undefined
|
correlation.type === 'query'
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
...correlation,
|
...correlation,
|
||||||
source: sourceDatasource,
|
source: sourceDatasource,
|
||||||
target: targetDatasource,
|
target: targetDatasource,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
correlationsLogger.logWarning(`Invalid correlation config: Missing source or target.`, {
|
|
||||||
source: JSON.stringify(sourceDatasource),
|
|
||||||
target: JSON.stringify(targetDatasource),
|
|
||||||
});
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
sourceDatasource &&
|
||||||
|
sourceDatasource?.uid !== undefined &&
|
||||||
|
targetDatasource?.uid === undefined &&
|
||||||
|
correlation.type === 'external'
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...correlation,
|
||||||
|
source: sourceDatasource,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
correlationsLogger.logWarning(`Invalid correlation config: Missing source or target.`, {
|
||||||
|
source: JSON.stringify(sourceDatasource),
|
||||||
|
target: JSON.stringify(targetDatasource),
|
||||||
|
});
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validSourceFilter = (correlation: CorrelationData | undefined): correlation is CorrelationData => !!correlation;
|
const validSourceFilter = (correlation: CorrelationData | undefined): correlation is CorrelationData => !!correlation;
|
||||||
|
@ -53,19 +53,31 @@ const decorateDataFrameWithInternalDataLinks = (dataFrame: DataFrame, correlatio
|
|||||||
dataFrame.fields.forEach((field) => {
|
dataFrame.fields.forEach((field) => {
|
||||||
field.config.links = field.config.links?.filter((link) => link.origin !== DataLinkConfigOrigin.Correlations) || [];
|
field.config.links = field.config.links?.filter((link) => link.origin !== DataLinkConfigOrigin.Correlations) || [];
|
||||||
correlations.map((correlation) => {
|
correlations.map((correlation) => {
|
||||||
if (correlation.config?.field === field.name) {
|
if (correlation.config.field === field.name) {
|
||||||
const targetQuery = correlation.config?.target || {};
|
if (correlation.type === 'query') {
|
||||||
field.config.links!.push({
|
const targetQuery = correlation.config.target || {};
|
||||||
internal: {
|
field.config.links!.push({
|
||||||
query: { ...targetQuery, datasource: { uid: correlation.target.uid } },
|
internal: {
|
||||||
datasourceUid: correlation.target.uid,
|
query: { ...targetQuery, datasource: { uid: correlation.target.uid } },
|
||||||
datasourceName: correlation.target.name,
|
datasourceUid: correlation.target.uid,
|
||||||
transformations: correlation.config?.transformations,
|
datasourceName: correlation.target.name,
|
||||||
},
|
},
|
||||||
url: '',
|
url: '',
|
||||||
title: correlation.label || correlation.target.name,
|
title: correlation.label || correlation.target.name,
|
||||||
origin: DataLinkConfigOrigin.Correlations,
|
origin: DataLinkConfigOrigin.Correlations,
|
||||||
});
|
meta: {
|
||||||
|
transformations: correlation.config.transformations,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (correlation.type === 'external') {
|
||||||
|
const externalTarget = correlation.config.target;
|
||||||
|
field.config.links!.push({
|
||||||
|
url: externalTarget.url,
|
||||||
|
title: correlation.label || 'External URL',
|
||||||
|
origin: DataLinkConfigOrigin.Correlations,
|
||||||
|
meta: { transformations: correlation.config?.transformations },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -42,6 +42,7 @@ const renderMenuItems = (
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
url={link.href}
|
url={link.href}
|
||||||
|
target={link.target}
|
||||||
className={styles.menuItem}
|
className={styles.menuItem}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { Field } from '@grafana/data';
|
import { Field, LinkTarget } from '@grafana/data';
|
||||||
|
|
||||||
import { TraceSpan } from './trace';
|
import { TraceSpan } from './trace';
|
||||||
|
|
||||||
@ -20,6 +20,7 @@ export type SpanLinkDef = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
field: Field;
|
field: Field;
|
||||||
type: SpanLinkType;
|
type: SpanLinkType;
|
||||||
|
target?: LinkTarget;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SpanLinkFunc = (span: TraceSpan) => SpanLinkDef[] | undefined;
|
export type SpanLinkFunc = (span: TraceSpan) => SpanLinkDef[] | undefined;
|
||||||
|
@ -1654,6 +1654,11 @@ function createMultiLinkDataFrame() {
|
|||||||
},
|
},
|
||||||
datasourceUid: 'loki1_uid',
|
datasourceUid: 'loki1_uid',
|
||||||
datasourceName: 'loki1',
|
datasourceName: 'loki1',
|
||||||
|
},
|
||||||
|
url: '',
|
||||||
|
title: 'Test',
|
||||||
|
origin: DataLinkConfigOrigin.Correlations,
|
||||||
|
meta: {
|
||||||
transformations: [
|
transformations: [
|
||||||
{
|
{
|
||||||
type: SupportedTransformationType.Regex,
|
type: SupportedTransformationType.Regex,
|
||||||
@ -1662,9 +1667,6 @@ function createMultiLinkDataFrame() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
url: '',
|
|
||||||
title: 'Test',
|
|
||||||
origin: DataLinkConfigOrigin.Correlations,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
internal: {
|
internal: {
|
||||||
@ -1673,6 +1675,11 @@ function createMultiLinkDataFrame() {
|
|||||||
},
|
},
|
||||||
datasourceUid: 'loki1_uid',
|
datasourceUid: 'loki1_uid',
|
||||||
datasourceName: 'loki1',
|
datasourceName: 'loki1',
|
||||||
|
},
|
||||||
|
url: '',
|
||||||
|
title: 'Test',
|
||||||
|
origin: DataLinkConfigOrigin.Correlations,
|
||||||
|
meta: {
|
||||||
transformations: [
|
transformations: [
|
||||||
{
|
{
|
||||||
type: SupportedTransformationType.Regex,
|
type: SupportedTransformationType.Regex,
|
||||||
@ -1681,9 +1688,6 @@ function createMultiLinkDataFrame() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
url: '',
|
|
||||||
title: 'Test2',
|
|
||||||
origin: DataLinkConfigOrigin.Correlations,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -110,6 +110,7 @@ export function createSpanLinkFactory({
|
|||||||
content: <Icon name="link" title={link.title || 'Link'} />,
|
content: <Icon name="link" title={link.title || 'Link'} />,
|
||||||
field: link.origin,
|
field: link.origin,
|
||||||
type: shouldCreatePyroscopeLink ? SpanLinkType.Profiles : SpanLinkType.Unknown,
|
type: shouldCreatePyroscopeLink ? SpanLinkType.Profiles : SpanLinkType.Unknown,
|
||||||
|
target: link.target,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -482,6 +482,7 @@ describe('decorateWithCorrelations', () => {
|
|||||||
source: datasourceInstance,
|
source: datasourceInstance,
|
||||||
target: datasourceInstance,
|
target: datasourceInstance,
|
||||||
provisioned: true,
|
provisioned: true,
|
||||||
|
type: 'query',
|
||||||
config: { field: panelData.series[0].fields[0].name },
|
config: { field: panelData.series[0].fields[0].name },
|
||||||
},
|
},
|
||||||
] as CorrelationData[];
|
] as CorrelationData[];
|
||||||
|
@ -127,9 +127,9 @@ export const decorateWithCorrelations = ({
|
|||||||
datasourceUid: defaultTargetDatasource.uid,
|
datasourceUid: defaultTargetDatasource.uid,
|
||||||
datasourceName: defaultTargetDatasource.name,
|
datasourceName: defaultTargetDatasource.name,
|
||||||
query: { datasource: { uid: defaultTargetDatasource.uid } },
|
query: { datasource: { uid: defaultTargetDatasource.uid } },
|
||||||
meta: {
|
},
|
||||||
correlationData: { resultField: field.name, vars: availableVars, origVars: availableVars },
|
meta: {
|
||||||
},
|
correlationData: { resultField: field.name, vars: availableVars, origVars: availableVars },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -281,6 +281,8 @@ describe('explore links utils', () => {
|
|||||||
query: { query: 'http_requests{app=${application} env=${environment}}' },
|
query: { query: 'http_requests{app=${application} env=${environment}}' },
|
||||||
datasourceUid: 'uid_1',
|
datasourceUid: 'uid_1',
|
||||||
datasourceName: 'test_ds',
|
datasourceName: 'test_ds',
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
transformations: [
|
transformations: [
|
||||||
{ type: SupportedTransformationType.Logfmt },
|
{ type: SupportedTransformationType.Logfmt },
|
||||||
{ type: SupportedTransformationType.Regex, expression: 'host=(dev|prod)', mapValue: 'environment' },
|
{ type: SupportedTransformationType.Regex, expression: 'host=(dev|prod)', mapValue: 'environment' },
|
||||||
@ -330,6 +332,8 @@ describe('explore links utils', () => {
|
|||||||
query: { query: 'http_requests{env=${msg}}' },
|
query: { query: 'http_requests{env=${msg}}' },
|
||||||
datasourceUid: 'uid_1',
|
datasourceUid: 'uid_1',
|
||||||
datasourceName: 'test_ds',
|
datasourceName: 'test_ds',
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
transformations: [
|
transformations: [
|
||||||
{ type: SupportedTransformationType.Regex, expression: 'fieldA=(asparagus|broccoli)' },
|
{ type: SupportedTransformationType.Regex, expression: 'fieldA=(asparagus|broccoli)' },
|
||||||
{ type: SupportedTransformationType.Regex, expression: 'fieldB=(apple|banana)' },
|
{ type: SupportedTransformationType.Regex, expression: 'fieldB=(apple|banana)' },
|
||||||
@ -372,6 +376,8 @@ describe('explore links utils', () => {
|
|||||||
query: { query: 'http_requests{env=${msg}}' },
|
query: { query: 'http_requests{env=${msg}}' },
|
||||||
datasourceUid: 'uid_1',
|
datasourceUid: 'uid_1',
|
||||||
datasourceName: 'test_ds',
|
datasourceName: 'test_ds',
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
transformations: [
|
transformations: [
|
||||||
{
|
{
|
||||||
type: SupportedTransformationType.Regex,
|
type: SupportedTransformationType.Regex,
|
||||||
@ -427,8 +433,8 @@ describe('explore links utils', () => {
|
|||||||
query: { query: 'http_requests{app=${application} isOnline=${online}}' },
|
query: { query: 'http_requests{app=${application} isOnline=${online}}' },
|
||||||
datasourceUid: 'uid_1',
|
datasourceUid: 'uid_1',
|
||||||
datasourceName: 'test_ds',
|
datasourceName: 'test_ds',
|
||||||
transformations: [{ type: SupportedTransformationType.Logfmt }],
|
|
||||||
},
|
},
|
||||||
|
meta: { transformations: [{ type: SupportedTransformationType.Logfmt }] },
|
||||||
};
|
};
|
||||||
|
|
||||||
const { field, range, dataFrame } = setup(transformationLink, true, {
|
const { field, range, dataFrame } = setup(transformationLink, true, {
|
||||||
@ -466,8 +472,8 @@ describe('explore links utils', () => {
|
|||||||
query: { query: 'http_requests{app=${application}}' },
|
query: { query: 'http_requests{app=${application}}' },
|
||||||
datasourceUid: 'uid_1',
|
datasourceUid: 'uid_1',
|
||||||
datasourceName: 'test_ds',
|
datasourceName: 'test_ds',
|
||||||
transformations: [{ type: SupportedTransformationType.Logfmt, field: 'fieldNamedInTransformation' }],
|
|
||||||
},
|
},
|
||||||
|
meta: { transformations: [{ type: SupportedTransformationType.Logfmt, field: 'fieldNamedInTransformation' }] },
|
||||||
};
|
};
|
||||||
|
|
||||||
// fieldWithLink has the transformation, but the transformation has defined fieldNamedInTransformation as its field to transform
|
// fieldWithLink has the transformation, but the transformation has defined fieldNamedInTransformation as its field to transform
|
||||||
@ -518,6 +524,8 @@ describe('explore links utils', () => {
|
|||||||
query: { query: 'http_requests{app=${application} env=${environment}}' },
|
query: { query: 'http_requests{app=${application} env=${environment}}' },
|
||||||
datasourceUid: 'uid_1',
|
datasourceUid: 'uid_1',
|
||||||
datasourceName: 'test_ds',
|
datasourceName: 'test_ds',
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
transformations: [
|
transformations: [
|
||||||
{
|
{
|
||||||
type: SupportedTransformationType.Regex,
|
type: SupportedTransformationType.Regex,
|
||||||
@ -598,6 +606,8 @@ describe('explore links utils', () => {
|
|||||||
query: { query: 'http_requests{app=${application} env=${diffVar}}' },
|
query: { query: 'http_requests{app=${application} env=${diffVar}}' },
|
||||||
datasourceUid: 'uid_1',
|
datasourceUid: 'uid_1',
|
||||||
datasourceName: 'test_ds',
|
datasourceName: 'test_ds',
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
transformations: [{ type: SupportedTransformationType.Logfmt }],
|
transformations: [{ type: SupportedTransformationType.Logfmt }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -623,6 +633,8 @@ describe('explore links utils', () => {
|
|||||||
query: { query: 'http_requests{app=test}' },
|
query: { query: 'http_requests{app=test}' },
|
||||||
datasourceUid: 'uid_1',
|
datasourceUid: 'uid_1',
|
||||||
datasourceName: 'test_ds',
|
datasourceName: 'test_ds',
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
transformations: [{ type: SupportedTransformationType.Logfmt }],
|
transformations: [{ type: SupportedTransformationType.Logfmt }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -49,7 +49,7 @@ const DATA_LINK_FILTERS: DataLinkFilter[] = [dataLinkHasRequiredPermissionsFilte
|
|||||||
* for internal links and undefined for non-internal links
|
* for internal links and undefined for non-internal links
|
||||||
*/
|
*/
|
||||||
export interface ExploreFieldLinkModel extends LinkModel<Field> {
|
export interface ExploreFieldLinkModel extends LinkModel<Field> {
|
||||||
variables?: VariableInterpolation[];
|
variables: VariableInterpolation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const DATA_LINK_USAGE_KEY = 'grafana_data_link_clicked';
|
const DATA_LINK_USAGE_KEY = 'grafana_data_link_clicked';
|
||||||
@ -65,7 +65,7 @@ export const exploreDataLinkPostProcessorFactory = (
|
|||||||
const { field, dataLinkScopedVars: vars, frame: dataFrame, link, linkModel } = options;
|
const { field, dataLinkScopedVars: vars, frame: dataFrame, link, linkModel } = options;
|
||||||
const { valueRowIndex: rowIndex } = options.config;
|
const { valueRowIndex: rowIndex } = options.config;
|
||||||
|
|
||||||
if (!link.internal || rowIndex === undefined) {
|
if (rowIndex === undefined) {
|
||||||
return linkModel;
|
return linkModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,57 +159,57 @@ export const getFieldLinksForExplore = (options: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const fieldLinks = links.map((link) => {
|
const fieldLinks = links.map((link) => {
|
||||||
if (!link.internal) {
|
let internalLinkSpecificVars: ScopedVars = {};
|
||||||
const replace: InterpolateFunction = (value, vars) =>
|
if (link.meta?.transformations) {
|
||||||
getTemplateSrv().replace(value, { ...vars, ...scopedVars });
|
link.meta?.transformations.forEach((transformation) => {
|
||||||
|
let fieldValue;
|
||||||
|
if (transformation.field) {
|
||||||
|
const transformField = dataFrame?.fields.find((field) => field.name === transformation.field);
|
||||||
|
fieldValue = transformField?.values[rowIndex];
|
||||||
|
} else {
|
||||||
|
fieldValue = field.values[rowIndex];
|
||||||
|
}
|
||||||
|
|
||||||
const linkModel = getLinkSrv().getDataLinkUIModel(link, replace, field);
|
internalLinkSpecificVars = {
|
||||||
if (!linkModel.title) {
|
...internalLinkSpecificVars,
|
||||||
linkModel.title = getTitleFromHref(linkModel.href);
|
...getTransformationVars(transformation, fieldValue, field.name),
|
||||||
}
|
};
|
||||||
return linkModel;
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const allVars = { ...scopedVars, ...internalLinkSpecificVars };
|
||||||
|
const variableData = getVariableUsageInfo(link, allVars);
|
||||||
|
let variables: VariableInterpolation[] = [];
|
||||||
|
|
||||||
|
// if the link has no variables (static link), add it with the right key but an empty value so we know what field the static link is associated with
|
||||||
|
if (variableData.variables.length === 0) {
|
||||||
|
const fieldName = field.name.toString();
|
||||||
|
variables.push({ variableName: fieldName, value: '', match: '' });
|
||||||
} else {
|
} else {
|
||||||
let internalLinkSpecificVars: ScopedVars = {};
|
variables = variableData.variables;
|
||||||
if (link.internal?.transformations) {
|
}
|
||||||
link.internal?.transformations.forEach((transformation) => {
|
if (variableData.allVariablesDefined) {
|
||||||
let fieldValue;
|
if (!link.internal) {
|
||||||
if (transformation.field) {
|
const replace: InterpolateFunction = (value, vars) =>
|
||||||
const transformField = dataFrame?.fields.find((field) => field.name === transformation.field);
|
getTemplateSrv().replace(value, { ...vars, ...allVars, ...scopedVars });
|
||||||
fieldValue = transformField?.values[rowIndex];
|
|
||||||
} else {
|
|
||||||
fieldValue = field.values[rowIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
internalLinkSpecificVars = {
|
const linkModel = getLinkSrv().getDataLinkUIModel(link, replace, field);
|
||||||
...internalLinkSpecificVars,
|
if (!linkModel.title) {
|
||||||
...getTransformationVars(transformation, fieldValue, field.name),
|
linkModel.title = getTitleFromHref(linkModel.href);
|
||||||
};
|
}
|
||||||
});
|
linkModel.target = '_blank';
|
||||||
}
|
return { ...linkModel, variables: variables };
|
||||||
|
|
||||||
const allVars = { ...scopedVars, ...internalLinkSpecificVars };
|
|
||||||
const variableData = getVariableUsageInfo(link, allVars);
|
|
||||||
let variables: VariableInterpolation[] = [];
|
|
||||||
|
|
||||||
// if the link has no variables (static link), add it with the right key but an empty value so we know what field the static link is associated with
|
|
||||||
if (variableData.variables.length === 0) {
|
|
||||||
const fieldName = field.name.toString();
|
|
||||||
variables.push({ variableName: fieldName, value: '', match: '' });
|
|
||||||
} else {
|
} else {
|
||||||
variables = variableData.variables;
|
const splitFnWithTracking = (options?: SplitOpenOptions<DataQuery>) => {
|
||||||
}
|
reportInteraction(DATA_LINK_USAGE_KEY, {
|
||||||
|
origin: link.origin || DataLinkConfigOrigin.Datasource,
|
||||||
|
app: CoreApp.Explore,
|
||||||
|
internal: true,
|
||||||
|
});
|
||||||
|
|
||||||
const splitFnWithTracking = (options?: SplitOpenOptions<DataQuery>) => {
|
splitOpenFn?.(options);
|
||||||
reportInteraction(DATA_LINK_USAGE_KEY, {
|
};
|
||||||
origin: link.origin || DataLinkConfigOrigin.Datasource,
|
|
||||||
app: CoreApp.Explore,
|
|
||||||
internal: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
splitOpenFn?.(options);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (variableData.allVariablesDefined) {
|
|
||||||
const internalLink = mapInternalLinkToExplore({
|
const internalLink = mapInternalLinkToExplore({
|
||||||
link,
|
link,
|
||||||
internalLink: link.internal,
|
internalLink: link.internal,
|
||||||
@ -221,9 +221,9 @@ export const getFieldLinksForExplore = (options: {
|
|||||||
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
|
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
|
||||||
});
|
});
|
||||||
return { ...internalLink, variables: variables };
|
return { ...internalLink, variables: variables };
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return fieldLinks.filter((link): link is ExploreFieldLinkModel => !!link);
|
return fieldLinks.filter((link): link is ExploreFieldLinkModel => !!link);
|
||||||
|
@ -448,6 +448,7 @@ describe('logParser', () => {
|
|||||||
},
|
},
|
||||||
title: 'test',
|
title: 'test',
|
||||||
target: '_self',
|
target: '_self',
|
||||||
|
variables: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const fieldWithVarLink: FieldDef = {
|
const fieldWithVarLink: FieldDef = {
|
||||||
|
@ -35,8 +35,16 @@ export const getAllFields = (
|
|||||||
export const createLogLineLinks = (hiddenFieldsWithLinks: FieldDef[]): FieldDef[] => {
|
export const createLogLineLinks = (hiddenFieldsWithLinks: FieldDef[]): FieldDef[] => {
|
||||||
let fieldsWithLinksFromVariableMap: FieldDef[] = [];
|
let fieldsWithLinksFromVariableMap: FieldDef[] = [];
|
||||||
hiddenFieldsWithLinks.forEach((linkField) => {
|
hiddenFieldsWithLinks.forEach((linkField) => {
|
||||||
linkField.links?.forEach((link: ExploreFieldLinkModel) => {
|
linkField.links?.forEach((link: LinkModel | ExploreFieldLinkModel) => {
|
||||||
if (link.variables) {
|
if ('variables' in link && link.variables.length > 0) {
|
||||||
|
// convert ExploreFieldLinkModel to LinkModel by omitting variables field
|
||||||
|
const fieldDefFromLink: LinkModel = {
|
||||||
|
href: link.href,
|
||||||
|
title: link.title,
|
||||||
|
origin: link.origin,
|
||||||
|
onClick: link.onClick,
|
||||||
|
target: link.target,
|
||||||
|
};
|
||||||
const variableKeys = link.variables.map((variable) => {
|
const variableKeys = link.variables.map((variable) => {
|
||||||
const varName = variable.variableName;
|
const varName = variable.variableName;
|
||||||
const fieldPath = variable.fieldPath ? `.${variable.fieldPath}` : '';
|
const fieldPath = variable.fieldPath ? `.${variable.fieldPath}` : '';
|
||||||
@ -46,7 +54,7 @@ export const createLogLineLinks = (hiddenFieldsWithLinks: FieldDef[]): FieldDef[
|
|||||||
fieldsWithLinksFromVariableMap.push({
|
fieldsWithLinksFromVariableMap.push({
|
||||||
keys: variableKeys,
|
keys: variableKeys,
|
||||||
values: variableValues,
|
values: variableValues,
|
||||||
links: [link],
|
links: [fieldDefFromLink],
|
||||||
fieldIndex: linkField.fieldIndex,
|
fieldIndex: linkField.fieldIndex,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -462,23 +462,30 @@
|
|||||||
},
|
},
|
||||||
"source-form": {
|
"source-form": {
|
||||||
"control-required": "This field is required.",
|
"control-required": "This field is required.",
|
||||||
"description": "You have used following variables in the target query: <1></1><2></2>A data point needs to provide values to all variables as fields or as transformations output to make the correlation button appear in the visualization.<4></4>Note: Not every variable needs to be explicitly defined below. A transformation such as <7>logfmt</7> will create variables for every key/value pair.",
|
"description": "A data point needs to provide values to all variables as fields or as transformations output to make the correlation button appear in the visualization.<1></1>Note: Not every variable needs to be explicitly defined below. A transformation such as <4>logfmt</4> will create variables for every key/value pair.",
|
||||||
"heading": "Variables used in the target query",
|
"description-external-pre": "You have used following variables in the target URL:",
|
||||||
|
"description-query-pre": "You have used following variables in the target query:",
|
||||||
|
"external-title": "Configure the data source that will use the URL (Step 3 of 3)",
|
||||||
|
"heading-external": "Variables used in the target URL",
|
||||||
|
"heading-query": "Variables used in the target query",
|
||||||
|
"query-title": "Configure the data source that will link to {{dataSourceName}} (Step 3 of 3)",
|
||||||
"results-description": "The link will be shown next to the value of this field",
|
"results-description": "The link will be shown next to the value of this field",
|
||||||
"results-label": "Results field",
|
"results-label": "Results field",
|
||||||
"results-required": "This field is required.",
|
"results-required": "This field is required.",
|
||||||
"source-description": "Results from selected source data source have links displayed in the panel",
|
"source-description": "Results from selected source data source have links displayed in the panel",
|
||||||
"source-label": "Source",
|
"source-label": "Source",
|
||||||
"sub-text": "<0>Define what data source will display the correlation, and what data will replace previously defined variables.</0>",
|
"sub-text": "<0>Define what data source will display the correlation, and what data will replace previously defined variables.</0>"
|
||||||
"title": "Configure the data source that will link to {{dataSourceName}} (Step 3 of 3)"
|
|
||||||
},
|
},
|
||||||
"sub-title": "Define how data living in different data sources relates to each other. Read more in the <2>documentation<1></1></2>",
|
"sub-title": "Define how data living in different data sources relates to each other. Read more in the <2>documentation<1></1></2>",
|
||||||
"target-form": {
|
"target-form": {
|
||||||
"control-rules": "This field is required.",
|
"control-rules": "This field is required.",
|
||||||
"sub-text": "<0>Define what data source the correlation will link to, and what query will run when the correlation is clicked.</0>",
|
"sub-text": "<0>Define what the correlation will link to. With the query type, a query will run when the correlation is clicked. With the external type, clicking the correlation will open a URL.</0>",
|
||||||
"target-description": "Specify which data source is queried when the link is clicked",
|
"target-description-external": "Specify the URL that will open when the link is clicked",
|
||||||
|
"target-description-query": "Specify which data source is queried when the link is clicked",
|
||||||
"target-label": "Target",
|
"target-label": "Target",
|
||||||
"title": "Setup the target for the correlation (Step 2 of 3)"
|
"target-type-description": "Specify the type of correlation",
|
||||||
|
"title": "Setup the target for the correlation (Step 2 of 3)",
|
||||||
|
"type-label": "Type"
|
||||||
},
|
},
|
||||||
"trans-details": {
|
"trans-details": {
|
||||||
"logfmt-description": "Parse provided field with logfmt to get variables",
|
"logfmt-description": "Parse provided field with logfmt to get variables",
|
||||||
|
@ -462,23 +462,30 @@
|
|||||||
},
|
},
|
||||||
"source-form": {
|
"source-form": {
|
||||||
"control-required": "Ŧĥįş ƒįęľđ įş řęqūįřęđ.",
|
"control-required": "Ŧĥįş ƒįęľđ įş řęqūįřęđ.",
|
||||||
"description": "Ÿőū ĥävę ūşęđ ƒőľľőŵįʼnģ väřįäþľęş įʼn ŧĥę ŧäřģęŧ qūęřy: <1></1><2></2>Å đäŧä pőįʼnŧ ʼnęęđş ŧő přővįđę väľūęş ŧő äľľ väřįäþľęş äş ƒįęľđş őř äş ŧřäʼnşƒőřmäŧįőʼnş őūŧpūŧ ŧő mäĸę ŧĥę čőřřęľäŧįőʼn þūŧŧőʼn äppęäř įʼn ŧĥę vįşūäľįžäŧįőʼn.<4></4>Ńőŧę: Ńőŧ ęvęřy väřįäþľę ʼnęęđş ŧő þę ęχpľįčįŧľy đęƒįʼnęđ þęľőŵ. Å ŧřäʼnşƒőřmäŧįőʼn şūčĥ äş <7>ľőģƒmŧ</7> ŵįľľ čřęäŧę väřįäþľęş ƒőř ęvęřy ĸęy/väľūę päįř.",
|
"description": "Å đäŧä pőįʼnŧ ʼnęęđş ŧő přővįđę väľūęş ŧő äľľ väřįäþľęş äş ƒįęľđş őř äş ŧřäʼnşƒőřmäŧįőʼnş őūŧpūŧ ŧő mäĸę ŧĥę čőřřęľäŧįőʼn þūŧŧőʼn äppęäř įʼn ŧĥę vįşūäľįžäŧįőʼn.<1></1>Ńőŧę: Ńőŧ ęvęřy väřįäþľę ʼnęęđş ŧő þę ęχpľįčįŧľy đęƒįʼnęđ þęľőŵ. Å ŧřäʼnşƒőřmäŧįőʼn şūčĥ äş <4>ľőģƒmŧ</4> ŵįľľ čřęäŧę väřįäþľęş ƒőř ęvęřy ĸęy/väľūę päįř.",
|
||||||
"heading": "Väřįäþľęş ūşęđ įʼn ŧĥę ŧäřģęŧ qūęřy",
|
"description-external-pre": "Ÿőū ĥävę ūşęđ ƒőľľőŵįʼnģ väřįäþľęş įʼn ŧĥę ŧäřģęŧ ŮŖĿ:",
|
||||||
|
"description-query-pre": "Ÿőū ĥävę ūşęđ ƒőľľőŵįʼnģ väřįäþľęş įʼn ŧĥę ŧäřģęŧ qūęřy:",
|
||||||
|
"external-title": "Cőʼnƒįģūřę ŧĥę đäŧä şőūřčę ŧĥäŧ ŵįľľ ūşę ŧĥę ŮŖĿ (Ŝŧęp 3 őƒ 3)",
|
||||||
|
"heading-external": "Väřįäþľęş ūşęđ įʼn ŧĥę ŧäřģęŧ ŮŖĿ",
|
||||||
|
"heading-query": "Väřįäþľęş ūşęđ įʼn ŧĥę ŧäřģęŧ qūęřy",
|
||||||
|
"query-title": "Cőʼnƒįģūřę ŧĥę đäŧä şőūřčę ŧĥäŧ ŵįľľ ľįʼnĸ ŧő {{dataSourceName}} (Ŝŧęp 3 őƒ 3)",
|
||||||
"results-description": "Ŧĥę ľįʼnĸ ŵįľľ þę şĥőŵʼn ʼnęχŧ ŧő ŧĥę väľūę őƒ ŧĥįş ƒįęľđ",
|
"results-description": "Ŧĥę ľįʼnĸ ŵįľľ þę şĥőŵʼn ʼnęχŧ ŧő ŧĥę väľūę őƒ ŧĥįş ƒįęľđ",
|
||||||
"results-label": "Ŗęşūľŧş ƒįęľđ",
|
"results-label": "Ŗęşūľŧş ƒįęľđ",
|
||||||
"results-required": "Ŧĥįş ƒįęľđ įş řęqūįřęđ.",
|
"results-required": "Ŧĥįş ƒįęľđ įş řęqūįřęđ.",
|
||||||
"source-description": "Ŗęşūľŧş ƒřőm şęľęčŧęđ şőūřčę đäŧä şőūřčę ĥävę ľįʼnĸş đįşpľäyęđ įʼn ŧĥę päʼnęľ",
|
"source-description": "Ŗęşūľŧş ƒřőm şęľęčŧęđ şőūřčę đäŧä şőūřčę ĥävę ľįʼnĸş đįşpľäyęđ įʼn ŧĥę päʼnęľ",
|
||||||
"source-label": "Ŝőūřčę",
|
"source-label": "Ŝőūřčę",
|
||||||
"sub-text": "<0>Đęƒįʼnę ŵĥäŧ đäŧä şőūřčę ŵįľľ đįşpľäy ŧĥę čőřřęľäŧįőʼn, äʼnđ ŵĥäŧ đäŧä ŵįľľ řępľäčę přęvįőūşľy đęƒįʼnęđ väřįäþľęş.</0>",
|
"sub-text": "<0>Đęƒįʼnę ŵĥäŧ đäŧä şőūřčę ŵįľľ đįşpľäy ŧĥę čőřřęľäŧįőʼn, äʼnđ ŵĥäŧ đäŧä ŵįľľ řępľäčę přęvįőūşľy đęƒįʼnęđ väřįäþľęş.</0>"
|
||||||
"title": "Cőʼnƒįģūřę ŧĥę đäŧä şőūřčę ŧĥäŧ ŵįľľ ľįʼnĸ ŧő {{dataSourceName}} (Ŝŧęp 3 őƒ 3)"
|
|
||||||
},
|
},
|
||||||
"sub-title": "Đęƒįʼnę ĥőŵ đäŧä ľįvįʼnģ įʼn đįƒƒęřęʼnŧ đäŧä şőūřčęş řęľäŧęş ŧő ęäčĥ őŧĥęř. Ŗęäđ mőřę įʼn ŧĥę <2>đőčūmęʼnŧäŧįőʼn<1></1></2>",
|
"sub-title": "Đęƒįʼnę ĥőŵ đäŧä ľįvįʼnģ įʼn đįƒƒęřęʼnŧ đäŧä şőūřčęş řęľäŧęş ŧő ęäčĥ őŧĥęř. Ŗęäđ mőřę įʼn ŧĥę <2>đőčūmęʼnŧäŧįőʼn<1></1></2>",
|
||||||
"target-form": {
|
"target-form": {
|
||||||
"control-rules": "Ŧĥįş ƒįęľđ įş řęqūįřęđ.",
|
"control-rules": "Ŧĥįş ƒįęľđ įş řęqūįřęđ.",
|
||||||
"sub-text": "<0>Đęƒįʼnę ŵĥäŧ đäŧä şőūřčę ŧĥę čőřřęľäŧįőʼn ŵįľľ ľįʼnĸ ŧő, äʼnđ ŵĥäŧ qūęřy ŵįľľ řūʼn ŵĥęʼn ŧĥę čőřřęľäŧįőʼn įş čľįčĸęđ.</0>",
|
"sub-text": "<0>Đęƒįʼnę ŵĥäŧ ŧĥę čőřřęľäŧįőʼn ŵįľľ ľįʼnĸ ŧő. Ŵįŧĥ ŧĥę qūęřy ŧypę, ä qūęřy ŵįľľ řūʼn ŵĥęʼn ŧĥę čőřřęľäŧįőʼn įş čľįčĸęđ. Ŵįŧĥ ŧĥę ęχŧęřʼnäľ ŧypę, čľįčĸįʼnģ ŧĥę čőřřęľäŧįőʼn ŵįľľ őpęʼn ä ŮŖĿ.</0>",
|
||||||
"target-description": "Ŝpęčįƒy ŵĥįčĥ đäŧä şőūřčę įş qūęřįęđ ŵĥęʼn ŧĥę ľįʼnĸ įş čľįčĸęđ",
|
"target-description-external": "Ŝpęčįƒy ŧĥę ŮŖĿ ŧĥäŧ ŵįľľ őpęʼn ŵĥęʼn ŧĥę ľįʼnĸ įş čľįčĸęđ",
|
||||||
|
"target-description-query": "Ŝpęčįƒy ŵĥįčĥ đäŧä şőūřčę įş qūęřįęđ ŵĥęʼn ŧĥę ľįʼnĸ įş čľįčĸęđ",
|
||||||
"target-label": "Ŧäřģęŧ",
|
"target-label": "Ŧäřģęŧ",
|
||||||
"title": "Ŝęŧūp ŧĥę ŧäřģęŧ ƒőř ŧĥę čőřřęľäŧįőʼn (Ŝŧęp 2 őƒ 3)"
|
"target-type-description": "Ŝpęčįƒy ŧĥę ŧypę őƒ čőřřęľäŧįőʼn",
|
||||||
|
"title": "Ŝęŧūp ŧĥę ŧäřģęŧ ƒőř ŧĥę čőřřęľäŧįőʼn (Ŝŧęp 2 őƒ 3)",
|
||||||
|
"type-label": "Ŧypę"
|
||||||
},
|
},
|
||||||
"trans-details": {
|
"trans-details": {
|
||||||
"logfmt-description": "Päřşę přővįđęđ ƒįęľđ ŵįŧĥ ľőģƒmŧ ŧő ģęŧ väřįäþľęş",
|
"logfmt-description": "Päřşę přővįđęđ ƒįęľđ ŵįŧĥ ľőģƒmŧ ŧő ģęŧ väřįäþľęş",
|
||||||
|
@ -4026,6 +4026,7 @@
|
|||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"CorrelationType": {
|
"CorrelationType": {
|
||||||
|
"description": "the type of correlation, either query for containing query information, or external for containing an external URL\n+enum",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"CounterResetHint": {
|
"CounterResetHint": {
|
||||||
|
Loading…
Reference in New Issue
Block a user