mirror of
https://github.com/grafana/grafana.git
synced 2024-11-22 08:56:43 -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.", "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": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[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 ...\`)", "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": [
|
||||
[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>;
|
||||
|
||||
origin?: DataLinkConfigOrigin;
|
||||
meta?: {
|
||||
correlationData?: ExploreCorrelationHelperData;
|
||||
transformations?: DataLinkTransformationConfig[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -78,10 +82,6 @@ export interface InternalDataLink<T extends DataQuery = any> {
|
||||
datasourceUid: string;
|
||||
datasourceName: string; // used as a title if `DataLink.title` is empty
|
||||
panelsState?: ExplorePanelsState;
|
||||
meta?: {
|
||||
correlationData?: ExploreCorrelationHelperData;
|
||||
};
|
||||
transformations?: DataLinkTransformationConfig[];
|
||||
range?: TimeRange;
|
||||
}
|
||||
|
||||
|
@ -40,11 +40,7 @@ export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkMod
|
||||
|
||||
const interpolatedQuery = interpolateObject(link.internal?.query, scopedVars, replaceVariables);
|
||||
const interpolatedPanelsState = interpolateObject(link.internal?.panelsState, scopedVars, replaceVariables);
|
||||
const interpolatedCorrelationData = interpolateObject(
|
||||
link.internal?.meta?.correlationData,
|
||||
scopedVars,
|
||||
replaceVariables
|
||||
);
|
||||
const interpolatedCorrelationData = interpolateObject(link.meta?.correlationData, scopedVars, replaceVariables);
|
||||
const title = link.title ? link.title : internalLink.datasourceName;
|
||||
|
||||
return {
|
||||
|
@ -11,6 +11,8 @@ import (
|
||||
"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
|
||||
func (s CorrelationsService) createCorrelation(ctx context.Context, cmd CreateCorrelationCommand) (Correlation, error) {
|
||||
correlation := Correlation{
|
||||
@ -25,6 +27,12 @@ func (s CorrelationsService) createCorrelation(ctx context.Context, cmd CreateCo
|
||||
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 {
|
||||
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
|
||||
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 {
|
||||
return ErrCorrelationNotFound
|
||||
}
|
||||
@ -235,7 +243,7 @@ func (s CorrelationsService) getCorrelationsBySourceUID(ctx context.Context, cmd
|
||||
return ErrSourceDataSourceDoesNotExists
|
||||
}
|
||||
// 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 {
|
||||
@ -256,12 +264,14 @@ func (s CorrelationsService) getCorrelations(ctx context.Context, cmd GetCorrela
|
||||
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
|
||||
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 {
|
||||
q.In("dss.uid", cmd.SourceUIDs)
|
||||
}
|
||||
|
||||
q.Where(VALID_TYPE_FILTER)
|
||||
|
||||
return q.Limit(int(cmd.Limit), int(offset)).Find(&result.Correlations)
|
||||
})
|
||||
if err != nil {
|
||||
@ -316,6 +326,7 @@ func (s CorrelationsService) createOrUpdateCorrelation(ctx context.Context, cmd
|
||||
Description: cmd.Description,
|
||||
Config: cmd.Config,
|
||||
Provisioned: false,
|
||||
Type: cmd.Type,
|
||||
}
|
||||
|
||||
found := false
|
||||
|
@ -14,12 +14,13 @@ var (
|
||||
ErrTargetDataSourceDoesNotExists = errors.New("target data source does not exist")
|
||||
ErrCorrelationNotFound = errors.New("correlation not found")
|
||||
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")
|
||||
ErrTransformationNotNested = errors.New("transformations must be nested under config")
|
||||
ErrTransformationRegexReqExp = errors.New("regex transformations require expression")
|
||||
ErrCorrelationsQuotaFailed = errors.New("error getting correlations quota")
|
||||
ErrCorrelationsQuotaReached = errors.New("correlations quota reached")
|
||||
ErrInvalidConfigType = errors.New("correlation contains non default value in config.type")
|
||||
)
|
||||
|
||||
const (
|
||||
@ -27,8 +28,15 @@ const (
|
||||
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
|
||||
|
||||
const (
|
||||
query CorrelationType = "query"
|
||||
external CorrelationType = "external"
|
||||
)
|
||||
|
||||
type Transformation struct {
|
||||
//Enum: regex,logfmt
|
||||
Type string `json:"type"`
|
||||
@ -37,13 +45,9 @@ type Transformation struct {
|
||||
MapValue string `json:"mapValue,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
TypeQuery CorrelationType = "query"
|
||||
)
|
||||
|
||||
func (t CorrelationType) Validate() error {
|
||||
if t != TypeQuery {
|
||||
return fmt.Errorf("%s: \"%s\"", ErrInvalidConfigType, t)
|
||||
if t != query && t != external {
|
||||
return fmt.Errorf("%s: \"%s\"", ErrInvalidType, t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -161,7 +165,7 @@ type CreateCorrelationCommand struct {
|
||||
Config CorrelationConfig `json:"config" binding:"Required"`
|
||||
// True if correlation was created with provisioning. This makes it read-only.
|
||||
Provisioned bool `json:"provisioned"`
|
||||
// correlation type, currently only valid value is "query"
|
||||
// correlation type
|
||||
Type CorrelationType `json:"type" binding:"Required"`
|
||||
}
|
||||
|
||||
@ -169,8 +173,8 @@ func (c CreateCorrelationCommand) Validate() error {
|
||||
if err := c.Type.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.TargetUID == nil && c.Type == TypeQuery {
|
||||
return fmt.Errorf("correlations of type \"%s\" must have a targetUID", TypeQuery)
|
||||
if c.TargetUID == nil && c.Type == query {
|
||||
return fmt.Errorf("correlations of type \"%s\" must have a targetUID", query)
|
||||
}
|
||||
|
||||
if err := c.Config.Transformations.Validate(); err != nil {
|
||||
|
@ -20,7 +20,7 @@ func TestCorrelationModels(t *testing.T) {
|
||||
OrgId: 1,
|
||||
TargetUID: &targetUid,
|
||||
Config: *config,
|
||||
Type: TypeQuery,
|
||||
Type: query,
|
||||
}
|
||||
|
||||
require.NoError(t, cmd.Validate())
|
||||
@ -30,7 +30,7 @@ func TestCorrelationModels(t *testing.T) {
|
||||
config := &CorrelationConfig{
|
||||
Field: "field",
|
||||
Target: map[string]any{},
|
||||
Type: TypeQuery,
|
||||
Type: query,
|
||||
}
|
||||
cmd := &CreateCorrelationCommand{
|
||||
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"
|
||||
var corrType = correlation["type"]
|
||||
if corrType == nil || corrType == "" {
|
||||
corrType = correlations.TypeQuery
|
||||
corrType = correlations.CorrelationType("query")
|
||||
}
|
||||
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
@ -229,6 +229,11 @@ func makeCreateCorrelationCommand(correlation map[string]any, SourceUID string,
|
||||
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
|
||||
}
|
||||
if err := createCommand.Validate(); err != nil {
|
||||
|
@ -104,7 +104,7 @@ func populateDB(t *testing.T, db db.DB, cfg *setting.Cfg) {
|
||||
Config: correlations.CorrelationConfig{
|
||||
Field: "field",
|
||||
Target: map[string]any{},
|
||||
Type: correlations.TypeQuery,
|
||||
Type: correlations.CorrelationType("query"),
|
||||
},
|
||||
}
|
||||
correlation, err := correlationsSvc.CreateCorrelation(context.Background(), cmd)
|
||||
|
@ -193,6 +193,11 @@ func (c TestContext) createCorrelation(cmd correlations.CreateCorrelationCommand
|
||||
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) {
|
||||
c.t.Helper()
|
||||
err := c.env.Server.HTTPServer.CorrelationsService.CreateOrUpdateCorrelation(context.Background(), cmd)
|
||||
|
@ -230,7 +230,7 @@ func TestIntegrationCreateCorrelation(t *testing.T) {
|
||||
description := "a description"
|
||||
label := "a label"
|
||||
fieldName := "fieldName"
|
||||
corrType := correlations.TypeQuery
|
||||
corrType := correlations.CorrelationType("query")
|
||||
transformation := correlations.Transformation{Type: "logfmt"}
|
||||
transformation2 := correlations.Transformation{Type: "regex", Expression: "testExpression", MapValue: "testVar"}
|
||||
res := ctx.Post(PostParams{
|
||||
|
@ -135,6 +135,7 @@ func TestIntegrationDeleteCorrelation(t *testing.T) {
|
||||
TargetUID: &writableDs,
|
||||
OrgId: writableDsOrgId,
|
||||
Provisioned: true,
|
||||
Type: correlations.CorrelationType("query"),
|
||||
})
|
||||
|
||||
res := ctx.Delete(DeleteParams{
|
||||
@ -160,6 +161,7 @@ func TestIntegrationDeleteCorrelation(t *testing.T) {
|
||||
SourceUID: writableDs,
|
||||
TargetUID: &writableDs,
|
||||
OrgId: writableDsOrgId,
|
||||
Type: correlations.CorrelationType("query"),
|
||||
})
|
||||
|
||||
res := ctx.Delete(DeleteParams{
|
||||
@ -192,6 +194,7 @@ func TestIntegrationDeleteCorrelation(t *testing.T) {
|
||||
SourceUID: writableDs,
|
||||
TargetUID: &readOnlyDS,
|
||||
OrgId: writableDsOrgId,
|
||||
Type: correlations.CorrelationType("query"),
|
||||
})
|
||||
|
||||
res := ctx.Delete(DeleteParams{
|
||||
@ -225,6 +228,7 @@ func TestIntegrationDeleteCorrelation(t *testing.T) {
|
||||
TargetUID: &readOnlyDS,
|
||||
OrgId: writableDsOrgId,
|
||||
Provisioned: false,
|
||||
Type: correlations.CorrelationType("query"),
|
||||
})
|
||||
|
||||
ctx.createCorrelation(correlations.CreateCorrelationCommand{
|
||||
@ -232,6 +236,7 @@ func TestIntegrationDeleteCorrelation(t *testing.T) {
|
||||
TargetUID: &readOnlyDS,
|
||||
OrgId: writableDsOrgId,
|
||||
Provisioned: true,
|
||||
Type: correlations.CorrelationType("query"),
|
||||
})
|
||||
|
||||
res := ctx.Delete(DeleteParams{
|
||||
|
@ -2,6 +2,7 @@ package correlations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
@ -38,8 +39,8 @@ func TestIntegrationCreateOrUpdateCorrelation(t *testing.T) {
|
||||
TargetUID: &dataSource.UID,
|
||||
OrgId: dataSource.OrgID,
|
||||
Label: "needs migration",
|
||||
Type: correlations.CorrelationType("query"),
|
||||
Config: correlations.CorrelationConfig{
|
||||
Type: correlations.TypeQuery,
|
||||
Field: "foo",
|
||||
Target: map[string]any{},
|
||||
Transformations: []correlations.Transformation{
|
||||
@ -54,8 +55,8 @@ func TestIntegrationCreateOrUpdateCorrelation(t *testing.T) {
|
||||
TargetUID: &dataSource.UID,
|
||||
OrgId: dataSource.OrgID,
|
||||
Label: "existing",
|
||||
Type: correlations.CorrelationType("query"),
|
||||
Config: correlations.CorrelationConfig{
|
||||
Type: correlations.TypeQuery,
|
||||
Field: "foo",
|
||||
Target: map[string]any{},
|
||||
Transformations: []correlations.Transformation{
|
||||
@ -65,6 +66,23 @@ func TestIntegrationCreateOrUpdateCorrelation(t *testing.T) {
|
||||
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) {
|
||||
// should be updated
|
||||
ctx.createOrUpdateCorrelation(correlations.CreateCorrelationCommand{
|
||||
@ -75,6 +93,7 @@ func TestIntegrationCreateOrUpdateCorrelation(t *testing.T) {
|
||||
Description: needsMigration.Description,
|
||||
Config: needsMigration.Config,
|
||||
Provisioned: true,
|
||||
Type: needsMigration.Type,
|
||||
})
|
||||
|
||||
// should be added
|
||||
@ -86,6 +105,7 @@ func TestIntegrationCreateOrUpdateCorrelation(t *testing.T) {
|
||||
Description: needsMigration.Description,
|
||||
Config: needsMigration.Config,
|
||||
Provisioned: true,
|
||||
Type: needsMigration.Type,
|
||||
})
|
||||
|
||||
res := ctx.Get(GetParams{
|
||||
@ -101,7 +121,7 @@ func TestIntegrationCreateOrUpdateCorrelation(t *testing.T) {
|
||||
err = json.Unmarshal(responseBody, &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, response.Correlations, 3)
|
||||
require.Len(t, response.Correlations, 4)
|
||||
|
||||
unordered := make(map[string]correlations.Correlation)
|
||||
for _, v := range response.Correlations {
|
||||
@ -117,4 +137,44 @@ func TestIntegrationCreateOrUpdateCorrelation(t *testing.T) {
|
||||
|
||||
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,
|
||||
TargetUID: &dsWithCorrelations.UID,
|
||||
OrgId: dsWithCorrelations.OrgID,
|
||||
Type: correlations.TypeQuery,
|
||||
Type: correlations.CorrelationType("query"),
|
||||
Config: correlations.CorrelationConfig{
|
||||
Field: "foo",
|
||||
Target: map[string]any{},
|
||||
|
@ -128,6 +128,7 @@ func TestIntegrationUpdateCorrelation(t *testing.T) {
|
||||
TargetUID: &writableDs,
|
||||
OrgId: writableDsOrgId,
|
||||
Provisioned: true,
|
||||
Type: correlations.CorrelationType("query"),
|
||||
})
|
||||
|
||||
res := ctx.Patch(PatchParams{
|
||||
@ -155,6 +156,7 @@ func TestIntegrationUpdateCorrelation(t *testing.T) {
|
||||
SourceUID: writableDs,
|
||||
TargetUID: &writableDs,
|
||||
OrgId: writableDsOrgId,
|
||||
Type: correlations.CorrelationType("query"),
|
||||
})
|
||||
|
||||
// no params
|
||||
@ -220,6 +222,7 @@ func TestIntegrationUpdateCorrelation(t *testing.T) {
|
||||
TargetUID: &writableDs,
|
||||
OrgId: writableDsOrgId,
|
||||
Label: "a label",
|
||||
Type: correlations.CorrelationType("query"),
|
||||
})
|
||||
|
||||
res := ctx.Patch(PatchParams{
|
||||
@ -249,9 +252,9 @@ func TestIntegrationUpdateCorrelation(t *testing.T) {
|
||||
OrgId: writableDsOrgId,
|
||||
Label: "0",
|
||||
Description: "0",
|
||||
Type: correlations.CorrelationType("query"),
|
||||
Config: correlations.CorrelationConfig{
|
||||
Field: "fieldName",
|
||||
Type: "query",
|
||||
Target: map[string]any{"expr": "foo"},
|
||||
},
|
||||
})
|
||||
|
@ -13793,6 +13793,7 @@
|
||||
}
|
||||
},
|
||||
"CorrelationType": {
|
||||
"description": "the type of correlation, either query for containing query information, or external for containing an external URL\n+enum",
|
||||
"type": "string"
|
||||
},
|
||||
"CounterResetHint": {
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
createRemoveCorrelationResponse,
|
||||
createUpdateCorrelationResponse,
|
||||
} from './__mocks__/useCorrelations.mocks';
|
||||
import { Correlation, CreateCorrelationParams } from './types';
|
||||
import { Correlation, CreateCorrelationParams, OmitUnion } from './types';
|
||||
|
||||
const renderWithContext = async (
|
||||
datasources: ConstructorParameters<typeof MockDataSourceSrv>[0] = {},
|
||||
@ -43,7 +43,7 @@ const renderWithContext = async (
|
||||
|
||||
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$/);
|
||||
if (matches?.groups) {
|
||||
const { sourceUID } = matches.groups;
|
||||
@ -54,7 +54,7 @@ const renderWithContext = async (
|
||||
|
||||
throw createFetchCorrelationsError();
|
||||
},
|
||||
patch: async (url: string, data: Omit<CreateCorrelationParams, 'sourceUID'>) => {
|
||||
patch: async (url: string, data: OmitUnion<CreateCorrelationParams, 'sourceUID'>) => {
|
||||
const matches = url.match(
|
||||
/^\/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 { 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 {
|
||||
Badge,
|
||||
@ -27,7 +27,7 @@ import { AccessControlAction } from 'app/types';
|
||||
import { AddCorrelationForm } from './Forms/AddCorrelationForm';
|
||||
import { EditCorrelationForm } from './Forms/EditCorrelationForm';
|
||||
import { EmptyCorrelationsCTA } from './components/EmptyCorrelationsCTA';
|
||||
import type { RemoveCorrelationParams } from './types';
|
||||
import type { Correlation, RemoveCorrelationParams } from './types';
|
||||
import { CorrelationData, useCorrelations } from './useCorrelations';
|
||||
|
||||
const sortDatasource: SortByFn<CorrelationData> = (a, b, column) =>
|
||||
@ -238,7 +238,7 @@ interface ExpandedRowProps {
|
||||
readOnly: boolean;
|
||||
onUpdated: () => void;
|
||||
}
|
||||
function ExpendedRow({ correlation: { source, target, ...correlation }, readOnly, onUpdated }: ExpandedRowProps) {
|
||||
function ExpendedRow({ correlation: { source, ...correlation }, readOnly, onUpdated }: ExpandedRowProps) {
|
||||
useEffect(
|
||||
() => reportInteraction('grafana_correlations_details_expanded'),
|
||||
// we only want to fire this on first render
|
||||
@ -246,13 +246,12 @@ function ExpendedRow({ correlation: { source, target, ...correlation }, readOnly
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<EditCorrelationForm
|
||||
correlation={{ ...correlation, sourceUID: source.uid, targetUID: target.uid }}
|
||||
onUpdated={onUpdated}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
let corr: Correlation =
|
||||
correlation.type === 'query'
|
||||
? { ...correlation, type: 'query', sourceUID: source.uid, targetUID: correlation.target.uid }
|
||||
: { ...correlation, type: 'external', sourceUID: source.uid };
|
||||
|
||||
return <EditCorrelationForm correlation={corr} onUpdated={onUpdated} readOnly={readOnly} />;
|
||||
}
|
||||
|
||||
const getDatasourceCellStyles = (theme: GrafanaTheme2) => ({
|
||||
@ -268,20 +267,22 @@ const getDatasourceCellStyles = (theme: GrafanaTheme2) => ({
|
||||
});
|
||||
|
||||
const DataSourceCell = memo(
|
||||
function DataSourceCell({
|
||||
cell: { value },
|
||||
}: CellProps<CorrelationData, CorrelationData['source'] | CorrelationData['target']>) {
|
||||
function DataSourceCell({ cell: { value } }: CellProps<CorrelationData, DataSourceInstanceSettings>) {
|
||||
const styles = useStyles2(getDatasourceCellStyles);
|
||||
|
||||
return (
|
||||
<span className={styles.root}>
|
||||
<img src={value.meta.info.logos.small} alt="" className={styles.dsLogo} />
|
||||
{value.name}
|
||||
{value?.name !== undefined && (
|
||||
<>
|
||||
<img src={value.meta.info.logos.small} alt="" className={styles.dsLogo} />
|
||||
{value.name}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
({ 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 = () => {
|
||||
const { control, formState, register, getValues } = useFormContext<FormDTO>();
|
||||
const styles = useStyles2(getStyles);
|
||||
@ -32,9 +61,13 @@ export const ConfigureCorrelationSourceForm = () => {
|
||||
const { correlation, readOnly } = useCorrelationsFormContext();
|
||||
|
||||
const currentTargetQuery = getValues('config.target');
|
||||
const currentType = getValues('type');
|
||||
const variables = getVariableUsageInfo(currentTargetQuery, {}).variables.map(
|
||||
(variable) => variable.variableName + (variable.fieldPath ? `.${variable.fieldPath}` : '')
|
||||
);
|
||||
const dataSourceName = getDatasourceSrv().getInstanceSettings(getValues('targetUID'))?.name;
|
||||
|
||||
const formText = getFormText(currentType, dataSourceName);
|
||||
|
||||
function VariableList() {
|
||||
return (
|
||||
@ -49,16 +82,9 @@ export const ConfigureCorrelationSourceForm = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const dataSourceName = getDatasourceSrv().getInstanceSettings(getValues('targetUID'))?.name;
|
||||
return (
|
||||
<>
|
||||
<FieldSet
|
||||
label={t(
|
||||
'correlations.source-form.title',
|
||||
'Configure the data source that will link to {{dataSourceName}} (Step 3 of 3)',
|
||||
{ dataSourceName }
|
||||
)}
|
||||
>
|
||||
<FieldSet label={formText.title}>
|
||||
<Trans i18nKey="correlations.source-form.sub-text">
|
||||
<p>
|
||||
Define what data source will display the correlation, and what data will replace previously defined
|
||||
@ -117,14 +143,14 @@ export const ConfigureCorrelationSourceForm = () => {
|
||||
</Field>
|
||||
{variables.length > 0 && (
|
||||
<Card>
|
||||
<Card.Heading>
|
||||
<Trans i18nKey="correlations.source-form.heading">Variables used in the target query</Trans>
|
||||
</Card.Heading>
|
||||
<Card.Heading>{formText.heading}</Card.Heading>
|
||||
<Card.Description>
|
||||
{formText.descriptionPre}
|
||||
<VariableList />
|
||||
<br />
|
||||
<Trans i18nKey="correlations.source-form.description">
|
||||
You have used following variables in the target query: <VariableList />
|
||||
<br />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.
|
||||
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.
|
||||
<br />
|
||||
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.
|
||||
|
@ -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 { Field, FieldSet } from '@grafana/ui';
|
||||
import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Field, FieldSet, Input, Select, useStyles2 } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||
|
||||
import { CorrelationType, ExternalTypeTarget } from '../types';
|
||||
|
||||
import { QueryEditorField } from './QueryEditorField';
|
||||
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 = () => {
|
||||
const { control, formState } = useFormContext<FormDTO>();
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext<FormDTO>();
|
||||
const withDsUID = (fn: Function) => (ds: DataSourceInstanceSettings) => fn(ds.uid);
|
||||
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 (
|
||||
<>
|
||||
<FieldSet label={t('correlations.target-form.title', 'Setup the target for the correlation (Step 2 of 3)')}>
|
||||
<Trans i18nKey="correlations.target-form.sub-text">
|
||||
<p>
|
||||
Define what data source the correlation will link to, and what query will run when the correlation is
|
||||
clicked.
|
||||
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.
|
||||
</p>
|
||||
</Trans>
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetUID"
|
||||
name="type"
|
||||
rules={{
|
||||
required: { value: true, message: t('correlations.target-form.control-rules', 'This field is required.') },
|
||||
}}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
render={({ field: { onChange, value, ...field } }) => (
|
||||
<Field
|
||||
label={t('correlations.target-form.target-label', 'Target')}
|
||||
description={t(
|
||||
'correlations.target-form.target-description',
|
||||
'Specify which data source is queried when the link is clicked'
|
||||
)}
|
||||
htmlFor="target"
|
||||
invalid={!!formState.errors.targetUID}
|
||||
error={formState.errors.targetUID?.message}
|
||||
label={t('correlations.target-form.type-label', 'Type')}
|
||||
description={t('correlations.target-form.target-type-description', 'Specify the type of correlation')}
|
||||
htmlFor="corrType"
|
||||
invalid={!!errors.type}
|
||||
>
|
||||
<DataSourcePicker
|
||||
onChange={withDsUID(onChange)}
|
||||
noDefault
|
||||
current={value}
|
||||
inputId="target"
|
||||
width={32}
|
||||
disabled={correlation !== undefined}
|
||||
<Select
|
||||
className={styles.typeSelect}
|
||||
value={correlationType}
|
||||
onChange={(value) => onChange(value.value)}
|
||||
options={Object.values(CORR_TYPES_SELECT)}
|
||||
aria-label="correlation type"
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
|
||||
<QueryEditorField
|
||||
name="config.target"
|
||||
dsUid={targetUID}
|
||||
invalid={!!formState.errors?.config?.target}
|
||||
error={formState.errors?.config?.target?.message}
|
||||
/>
|
||||
{correlationType === 'query' &&
|
||||
(() => {
|
||||
assertIsQueryTypeError(errors);
|
||||
// the assert above will make sure the form dto, which can be either external or query, is for query
|
||||
// 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>
|
||||
</>
|
||||
);
|
||||
|
@ -52,12 +52,14 @@ export const QueryEditorField = ({ dsUid, invalid, error, name }: Props) => {
|
||||
name={name}
|
||||
rules={{
|
||||
validate: {
|
||||
hasQueryEditor: () =>
|
||||
QueryEditor !== undefined ||
|
||||
t(
|
||||
'correlations.query-editor.control-rules',
|
||||
'The selected target data source must export a query editor.'
|
||||
),
|
||||
hasQueryEditor: (_, formVals) => {
|
||||
return formVals.type === 'query' && QueryEditor === undefined
|
||||
? t(
|
||||
'correlations.query-editor.control-rules',
|
||||
'The selected target data source must export a query editor.'
|
||||
)
|
||||
: true;
|
||||
},
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => {
|
||||
|
@ -1,18 +1,36 @@
|
||||
import { DeepMap, FieldError, FieldErrors } from 'react-hook-form';
|
||||
|
||||
import { SupportedTransformationType } from '@grafana/data';
|
||||
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;
|
||||
targetUID: string;
|
||||
label: string;
|
||||
description: string;
|
||||
type: CorrelationType;
|
||||
config: CorrelationConfig;
|
||||
type: 'query';
|
||||
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 = {
|
||||
type: SupportedTransformationType;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Correlation } from '../types';
|
||||
|
||||
type CorrelationBaseData = Pick<Correlation, 'uid' | 'sourceUID' | 'targetUID'>;
|
||||
type CorrelationBaseData = Pick<Correlation, 'uid' | 'sourceUID'>;
|
||||
|
||||
export const getInputId = (inputName: string, correlation?: CorrelationBaseData) => {
|
||||
if (!correlation) {
|
||||
|
@ -26,30 +26,50 @@ export interface RemoveCorrelationResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type CorrelationType = 'query';
|
||||
export type CorrelationType = 'query' | 'external';
|
||||
|
||||
export interface CorrelationConfig {
|
||||
export type ExternalTypeTarget = { url: string };
|
||||
|
||||
export type CorrelationConfigQuery = {
|
||||
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[];
|
||||
}
|
||||
};
|
||||
|
||||
export interface Correlation {
|
||||
export type CorrelationConfigExternal = {
|
||||
field: string;
|
||||
target: ExternalTypeTarget; // For external, this simply contains a URL
|
||||
transformations?: DataLinkTransformationConfig[];
|
||||
};
|
||||
|
||||
type CorrelationBase = {
|
||||
uid: string;
|
||||
sourceUID: string;
|
||||
targetUID: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
provisioned: boolean;
|
||||
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 = {
|
||||
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 CreateCorrelationParams = Omit<Correlation, 'uid' | 'provisioned'>;
|
||||
export type UpdateCorrelationParams = Omit<Correlation, 'targetUID' | 'provisioned'>;
|
||||
export type CreateCorrelationParams = OmitUnion<Correlation, 'uid' | 'provisioned'>;
|
||||
export type UpdateCorrelationParams = OmitUnion<Correlation, 'targetUID' | 'provisioned'>;
|
||||
|
@ -7,6 +7,8 @@ import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||
|
||||
import {
|
||||
Correlation,
|
||||
CorrelationExternal,
|
||||
CorrelationQuery,
|
||||
CreateCorrelationParams,
|
||||
CreateCorrelationResponse,
|
||||
GetCorrelationsParams,
|
||||
@ -24,10 +26,14 @@ export interface CorrelationsResponse {
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface CorrelationData extends Omit<Correlation, 'sourceUID' | 'targetUID'> {
|
||||
source: DataSourceInstanceSettings;
|
||||
target: DataSourceInstanceSettings;
|
||||
}
|
||||
export type CorrelationData =
|
||||
| (Omit<CorrelationExternal, 'sourceUID'> & {
|
||||
source: DataSourceInstanceSettings;
|
||||
})
|
||||
| (Omit<CorrelationQuery, 'sourceUID' | 'targetUID'> & {
|
||||
source: DataSourceInstanceSettings;
|
||||
target: DataSourceInstanceSettings;
|
||||
});
|
||||
|
||||
export interface CorrelationsData {
|
||||
correlations: CorrelationData[];
|
||||
@ -36,13 +42,10 @@ export interface CorrelationsData {
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
const toEnrichedCorrelationData = ({
|
||||
sourceUID,
|
||||
targetUID,
|
||||
...correlation
|
||||
}: Correlation): CorrelationData | undefined => {
|
||||
const toEnrichedCorrelationData = ({ sourceUID, ...correlation }: Correlation): CorrelationData | undefined => {
|
||||
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.
|
||||
// This logging is to check if there are any customers who did not migrate existing correlations.
|
||||
@ -54,21 +57,33 @@ const toEnrichedCorrelationData = ({
|
||||
if (
|
||||
sourceDatasource &&
|
||||
sourceDatasource?.uid !== undefined &&
|
||||
targetDatasource &&
|
||||
targetDatasource.uid !== undefined
|
||||
targetDatasource?.uid !== undefined &&
|
||||
correlation.type === 'query'
|
||||
) {
|
||||
return {
|
||||
...correlation,
|
||||
source: sourceDatasource,
|
||||
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;
|
||||
|
@ -53,19 +53,31 @@ const decorateDataFrameWithInternalDataLinks = (dataFrame: DataFrame, correlatio
|
||||
dataFrame.fields.forEach((field) => {
|
||||
field.config.links = field.config.links?.filter((link) => link.origin !== DataLinkConfigOrigin.Correlations) || [];
|
||||
correlations.map((correlation) => {
|
||||
if (correlation.config?.field === field.name) {
|
||||
const targetQuery = correlation.config?.target || {};
|
||||
field.config.links!.push({
|
||||
internal: {
|
||||
query: { ...targetQuery, datasource: { uid: correlation.target.uid } },
|
||||
datasourceUid: correlation.target.uid,
|
||||
datasourceName: correlation.target.name,
|
||||
transformations: correlation.config?.transformations,
|
||||
},
|
||||
url: '',
|
||||
title: correlation.label || correlation.target.name,
|
||||
origin: DataLinkConfigOrigin.Correlations,
|
||||
});
|
||||
if (correlation.config.field === field.name) {
|
||||
if (correlation.type === 'query') {
|
||||
const targetQuery = correlation.config.target || {};
|
||||
field.config.links!.push({
|
||||
internal: {
|
||||
query: { ...targetQuery, datasource: { uid: correlation.target.uid } },
|
||||
datasourceUid: correlation.target.uid,
|
||||
datasourceName: correlation.target.name,
|
||||
},
|
||||
url: '',
|
||||
title: correlation.label || correlation.target.name,
|
||||
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
|
||||
}
|
||||
url={link.href}
|
||||
target={link.target}
|
||||
className={styles.menuItem}
|
||||
/>
|
||||
));
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Field } from '@grafana/data';
|
||||
import { Field, LinkTarget } from '@grafana/data';
|
||||
|
||||
import { TraceSpan } from './trace';
|
||||
|
||||
@ -20,6 +20,7 @@ export type SpanLinkDef = {
|
||||
title?: string;
|
||||
field: Field;
|
||||
type: SpanLinkType;
|
||||
target?: LinkTarget;
|
||||
};
|
||||
|
||||
export type SpanLinkFunc = (span: TraceSpan) => SpanLinkDef[] | undefined;
|
||||
|
@ -1654,6 +1654,11 @@ function createMultiLinkDataFrame() {
|
||||
},
|
||||
datasourceUid: 'loki1_uid',
|
||||
datasourceName: 'loki1',
|
||||
},
|
||||
url: '',
|
||||
title: 'Test',
|
||||
origin: DataLinkConfigOrigin.Correlations,
|
||||
meta: {
|
||||
transformations: [
|
||||
{
|
||||
type: SupportedTransformationType.Regex,
|
||||
@ -1662,9 +1667,6 @@ function createMultiLinkDataFrame() {
|
||||
},
|
||||
],
|
||||
},
|
||||
url: '',
|
||||
title: 'Test',
|
||||
origin: DataLinkConfigOrigin.Correlations,
|
||||
},
|
||||
{
|
||||
internal: {
|
||||
@ -1673,6 +1675,11 @@ function createMultiLinkDataFrame() {
|
||||
},
|
||||
datasourceUid: 'loki1_uid',
|
||||
datasourceName: 'loki1',
|
||||
},
|
||||
url: '',
|
||||
title: 'Test',
|
||||
origin: DataLinkConfigOrigin.Correlations,
|
||||
meta: {
|
||||
transformations: [
|
||||
{
|
||||
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'} />,
|
||||
field: link.origin,
|
||||
type: shouldCreatePyroscopeLink ? SpanLinkType.Profiles : SpanLinkType.Unknown,
|
||||
target: link.target,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -482,6 +482,7 @@ describe('decorateWithCorrelations', () => {
|
||||
source: datasourceInstance,
|
||||
target: datasourceInstance,
|
||||
provisioned: true,
|
||||
type: 'query',
|
||||
config: { field: panelData.series[0].fields[0].name },
|
||||
},
|
||||
] as CorrelationData[];
|
||||
|
@ -127,9 +127,9 @@ export const decorateWithCorrelations = ({
|
||||
datasourceUid: defaultTargetDatasource.uid,
|
||||
datasourceName: defaultTargetDatasource.name,
|
||||
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}}' },
|
||||
datasourceUid: 'uid_1',
|
||||
datasourceName: 'test_ds',
|
||||
},
|
||||
meta: {
|
||||
transformations: [
|
||||
{ type: SupportedTransformationType.Logfmt },
|
||||
{ type: SupportedTransformationType.Regex, expression: 'host=(dev|prod)', mapValue: 'environment' },
|
||||
@ -330,6 +332,8 @@ describe('explore links utils', () => {
|
||||
query: { query: 'http_requests{env=${msg}}' },
|
||||
datasourceUid: 'uid_1',
|
||||
datasourceName: 'test_ds',
|
||||
},
|
||||
meta: {
|
||||
transformations: [
|
||||
{ type: SupportedTransformationType.Regex, expression: 'fieldA=(asparagus|broccoli)' },
|
||||
{ type: SupportedTransformationType.Regex, expression: 'fieldB=(apple|banana)' },
|
||||
@ -372,6 +376,8 @@ describe('explore links utils', () => {
|
||||
query: { query: 'http_requests{env=${msg}}' },
|
||||
datasourceUid: 'uid_1',
|
||||
datasourceName: 'test_ds',
|
||||
},
|
||||
meta: {
|
||||
transformations: [
|
||||
{
|
||||
type: SupportedTransformationType.Regex,
|
||||
@ -427,8 +433,8 @@ describe('explore links utils', () => {
|
||||
query: { query: 'http_requests{app=${application} isOnline=${online}}' },
|
||||
datasourceUid: 'uid_1',
|
||||
datasourceName: 'test_ds',
|
||||
transformations: [{ type: SupportedTransformationType.Logfmt }],
|
||||
},
|
||||
meta: { transformations: [{ type: SupportedTransformationType.Logfmt }] },
|
||||
};
|
||||
|
||||
const { field, range, dataFrame } = setup(transformationLink, true, {
|
||||
@ -466,8 +472,8 @@ describe('explore links utils', () => {
|
||||
query: { query: 'http_requests{app=${application}}' },
|
||||
datasourceUid: 'uid_1',
|
||||
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
|
||||
@ -518,6 +524,8 @@ describe('explore links utils', () => {
|
||||
query: { query: 'http_requests{app=${application} env=${environment}}' },
|
||||
datasourceUid: 'uid_1',
|
||||
datasourceName: 'test_ds',
|
||||
},
|
||||
meta: {
|
||||
transformations: [
|
||||
{
|
||||
type: SupportedTransformationType.Regex,
|
||||
@ -598,6 +606,8 @@ describe('explore links utils', () => {
|
||||
query: { query: 'http_requests{app=${application} env=${diffVar}}' },
|
||||
datasourceUid: 'uid_1',
|
||||
datasourceName: 'test_ds',
|
||||
},
|
||||
meta: {
|
||||
transformations: [{ type: SupportedTransformationType.Logfmt }],
|
||||
},
|
||||
};
|
||||
@ -623,6 +633,8 @@ describe('explore links utils', () => {
|
||||
query: { query: 'http_requests{app=test}' },
|
||||
datasourceUid: 'uid_1',
|
||||
datasourceName: 'test_ds',
|
||||
},
|
||||
meta: {
|
||||
transformations: [{ type: SupportedTransformationType.Logfmt }],
|
||||
},
|
||||
};
|
||||
|
@ -49,7 +49,7 @@ const DATA_LINK_FILTERS: DataLinkFilter[] = [dataLinkHasRequiredPermissionsFilte
|
||||
* for internal links and undefined for non-internal links
|
||||
*/
|
||||
export interface ExploreFieldLinkModel extends LinkModel<Field> {
|
||||
variables?: VariableInterpolation[];
|
||||
variables: VariableInterpolation[];
|
||||
}
|
||||
|
||||
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 { valueRowIndex: rowIndex } = options.config;
|
||||
|
||||
if (!link.internal || rowIndex === undefined) {
|
||||
if (rowIndex === undefined) {
|
||||
return linkModel;
|
||||
}
|
||||
|
||||
@ -159,57 +159,57 @@ export const getFieldLinksForExplore = (options: {
|
||||
});
|
||||
|
||||
const fieldLinks = links.map((link) => {
|
||||
if (!link.internal) {
|
||||
const replace: InterpolateFunction = (value, vars) =>
|
||||
getTemplateSrv().replace(value, { ...vars, ...scopedVars });
|
||||
let internalLinkSpecificVars: ScopedVars = {};
|
||||
if (link.meta?.transformations) {
|
||||
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);
|
||||
if (!linkModel.title) {
|
||||
linkModel.title = getTitleFromHref(linkModel.href);
|
||||
}
|
||||
return linkModel;
|
||||
internalLinkSpecificVars = {
|
||||
...internalLinkSpecificVars,
|
||||
...getTransformationVars(transformation, fieldValue, field.name),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
let internalLinkSpecificVars: ScopedVars = {};
|
||||
if (link.internal?.transformations) {
|
||||
link.internal?.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];
|
||||
}
|
||||
variables = variableData.variables;
|
||||
}
|
||||
if (variableData.allVariablesDefined) {
|
||||
if (!link.internal) {
|
||||
const replace: InterpolateFunction = (value, vars) =>
|
||||
getTemplateSrv().replace(value, { ...vars, ...allVars, ...scopedVars });
|
||||
|
||||
internalLinkSpecificVars = {
|
||||
...internalLinkSpecificVars,
|
||||
...getTransformationVars(transformation, fieldValue, field.name),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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: '' });
|
||||
const linkModel = getLinkSrv().getDataLinkUIModel(link, replace, field);
|
||||
if (!linkModel.title) {
|
||||
linkModel.title = getTitleFromHref(linkModel.href);
|
||||
}
|
||||
linkModel.target = '_blank';
|
||||
return { ...linkModel, variables: variables };
|
||||
} 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>) => {
|
||||
reportInteraction(DATA_LINK_USAGE_KEY, {
|
||||
origin: link.origin || DataLinkConfigOrigin.Datasource,
|
||||
app: CoreApp.Explore,
|
||||
internal: true,
|
||||
});
|
||||
splitOpenFn?.(options);
|
||||
};
|
||||
|
||||
splitOpenFn?.(options);
|
||||
};
|
||||
|
||||
if (variableData.allVariablesDefined) {
|
||||
const internalLink = mapInternalLinkToExplore({
|
||||
link,
|
||||
internalLink: link.internal,
|
||||
@ -221,9 +221,9 @@ export const getFieldLinksForExplore = (options: {
|
||||
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
|
||||
});
|
||||
return { ...internalLink, variables: variables };
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
return fieldLinks.filter((link): link is ExploreFieldLinkModel => !!link);
|
||||
|
@ -448,6 +448,7 @@ describe('logParser', () => {
|
||||
},
|
||||
title: 'test',
|
||||
target: '_self',
|
||||
variables: [],
|
||||
};
|
||||
|
||||
const fieldWithVarLink: FieldDef = {
|
||||
|
@ -35,8 +35,16 @@ export const getAllFields = (
|
||||
export const createLogLineLinks = (hiddenFieldsWithLinks: FieldDef[]): FieldDef[] => {
|
||||
let fieldsWithLinksFromVariableMap: FieldDef[] = [];
|
||||
hiddenFieldsWithLinks.forEach((linkField) => {
|
||||
linkField.links?.forEach((link: ExploreFieldLinkModel) => {
|
||||
if (link.variables) {
|
||||
linkField.links?.forEach((link: LinkModel | ExploreFieldLinkModel) => {
|
||||
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 varName = variable.variableName;
|
||||
const fieldPath = variable.fieldPath ? `.${variable.fieldPath}` : '';
|
||||
@ -46,7 +54,7 @@ export const createLogLineLinks = (hiddenFieldsWithLinks: FieldDef[]): FieldDef[
|
||||
fieldsWithLinksFromVariableMap.push({
|
||||
keys: variableKeys,
|
||||
values: variableValues,
|
||||
links: [link],
|
||||
links: [fieldDefFromLink],
|
||||
fieldIndex: linkField.fieldIndex,
|
||||
});
|
||||
}
|
||||
|
@ -462,23 +462,30 @@
|
||||
},
|
||||
"source-form": {
|
||||
"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.",
|
||||
"heading": "Variables used in the target query",
|
||||
"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.",
|
||||
"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-label": "Results field",
|
||||
"results-required": "This field is required.",
|
||||
"source-description": "Results from selected source data source have links displayed in the panel",
|
||||
"source-label": "Source",
|
||||
"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-text": "<0>Define what data source will display the correlation, and what data will replace previously defined variables.</0>"
|
||||
},
|
||||
"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": {
|
||||
"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>",
|
||||
"target-description": "Specify which data source is queried when the link is clicked",
|
||||
"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-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",
|
||||
"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": {
|
||||
"logfmt-description": "Parse provided field with logfmt to get variables",
|
||||
|
@ -462,23 +462,30 @@
|
||||
},
|
||||
"source-form": {
|
||||
"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äįř.",
|
||||
"heading": "Väřįäþľęş ūşęđ įʼn ŧĥę ŧäřģęŧ qūęřy",
|
||||
"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äįř.",
|
||||
"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-label": "Ŗęşūľŧş ƒįęľđ",
|
||||
"results-required": "Ŧĥįş ƒįęľđ įş řęqūįřęđ.",
|
||||
"source-description": "Ŗęşūľŧş ƒřőm şęľęčŧęđ şőūřčę đäŧä şőūřčę ĥävę ľįʼnĸş đįşpľäyęđ įʼn ŧĥę päʼnęľ",
|
||||
"source-label": "Ŝőūřčę",
|
||||
"sub-text": "<0>Đęƒįʼnę ŵĥäŧ đäŧä şőūřčę ŵįľľ đįşpľäy ŧĥę čőřřęľäŧįőʼn, äʼnđ ŵĥäŧ đäŧä ŵįľľ řępľäčę přęvįőūşľy đęƒįʼnęđ väřįäþľęş.</0>",
|
||||
"title": "Cőʼnƒįģūřę ŧĥę đäŧä şőūřčę ŧĥäŧ ŵįľľ ľįʼnĸ ŧő {{dataSourceName}} (Ŝŧęp 3 őƒ 3)"
|
||||
"sub-text": "<0>Đęƒįʼnę ŵĥäŧ đäŧä şőūřčę ŵįľľ đįşpľäy ŧĥę čőřřęľäŧįőʼn, äʼnđ ŵĥäŧ đäŧä ŵįľľ řępľäčę přęvįőūşľy đęƒįʼnęđ väřįäþľęş.</0>"
|
||||
},
|
||||
"sub-title": "Đęƒįʼnę ĥőŵ đäŧä ľįvįʼnģ įʼn đįƒƒęřęʼnŧ đäŧä şőūřčęş řęľäŧęş ŧő ęäčĥ őŧĥęř. Ŗęäđ mőřę įʼn ŧĥę <2>đőčūmęʼnŧäŧįőʼn<1></1></2>",
|
||||
"target-form": {
|
||||
"control-rules": "Ŧĥįş ƒįęľđ įş řęqūįřęđ.",
|
||||
"sub-text": "<0>Đęƒįʼnę ŵĥäŧ đäŧä şőūřčę ŧĥę čőřřęľäŧįőʼn ŵįľľ ľįʼnĸ ŧő, äʼnđ ŵĥäŧ qūęřy ŵįľľ řūʼn ŵĥęʼn ŧĥę čőřřęľäŧįőʼn įş čľįčĸęđ.</0>",
|
||||
"target-description": "Ŝpęčįƒy ŵĥįčĥ đäŧä şőūřčę įş qūęřįęđ ŵĥęʼn ŧĥę ľįʼnĸ įş čľįčĸęđ",
|
||||
"sub-text": "<0>Đęƒįʼnę ŵĥäŧ ŧĥę čőřřęľäŧįőʼn ŵįľľ ľįʼnĸ ŧő. Ŵįŧĥ ŧĥę qūęřy ŧypę, ä qūęřy ŵįľľ řūʼn ŵĥęʼn ŧĥę čőřřęľäŧįőʼn įş čľįčĸęđ. Ŵįŧĥ ŧĥę ęχŧęřʼnäľ ŧypę, čľįčĸįʼnģ ŧĥę čőřřęľäŧįőʼn ŵįľľ őpęʼn ä ŮŖĿ.</0>",
|
||||
"target-description-external": "Ŝpęčįƒy ŧĥę ŮŖĿ ŧĥäŧ ŵįľľ őpęʼn ŵĥęʼn ŧĥę ľįʼnĸ įş čľįčĸęđ",
|
||||
"target-description-query": "Ŝpęčįƒy ŵĥįčĥ đäŧä şőūřčę įş qūęřįęđ ŵĥęʼn ŧĥę ľįʼnĸ įş čľįčĸęđ",
|
||||
"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": {
|
||||
"logfmt-description": "Päřşę přővįđęđ ƒįęľđ ŵįŧĥ ľőģƒmŧ ŧő ģęŧ väřįäþľęş",
|
||||
|
@ -4026,6 +4026,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"CorrelationType": {
|
||||
"description": "the type of correlation, either query for containing query information, or external for containing an external URL\n+enum",
|
||||
"type": "string"
|
||||
},
|
||||
"CounterResetHint": {
|
||||
|
Loading…
Reference in New Issue
Block a user