From 06dfe2156f79194fea991b828692a900c4f43b98 Mon Sep 17 00:00:00 2001 From: Kristina Date: Wed, 22 Feb 2023 06:53:03 -0600 Subject: [PATCH] Explore: Add transformations to correlation data links (#61799) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bring in source from database * bring in transformations from database * add regex transformations to scopevar * Consolidate types, add better example, cleanup * Add var only if match * Change ScopedVar to not require text, do not leak transformation-made variables between links * Add mappings and start implementing logfmt * Add mappings and start implementing logfmt * Remove mappings, turn off global regex * Add example yaml and omit transformations if empty * Fix the yaml * Add logfmt transformation * Cleanup transformations and yaml * add transformation field to FE types and use it, safeStringify logfmt values * Add tests, only safe stringify if non-string, fix bug with safe stringify where it would return empty string with false value * Add test for transformation field * Do not add null transformations object * Break out transformation logic, add tests to backend code * Fix lint errors I understand 😅 * Fix the backend lint error * Remove unnecessary code and mark new Transformations object as internal * Add support for named capture groups * Remove type assertion * Remove variable name from transformation * Add test for overriding regexes * Add back variable name field, but change to mapValue * fix go api test * Change transformation types to enum, add better provisioning checks for bad type name and format * Check for expression with regex transformations --- .betterer.results | 3 - package.json | 2 +- packages/grafana-data/src/types/ScopedVars.ts | 2 +- packages/grafana-data/src/types/dataLink.ts | 15 ++ pkg/services/correlations/database.go | 3 + pkg/services/correlations/models.go | 50 +++- .../provisioning/datasources/datasources.go | 4 + .../correlations/correlations_create_test.go | 10 +- .../correlations/correlations_read_test.go | 3 + .../correlations/correlations_update_test.go | 4 +- public/app/core/utils/explore.ts | 2 +- .../features/correlations/transformations.ts | 34 +++ public/app/features/correlations/types.ts | 4 + public/app/features/correlations/utils.ts | 1 + .../app/features/explore/utils/links.test.ts | 228 ++++++++++++++++++ public/app/features/explore/utils/links.ts | 23 +- yarn.lock | 10 +- 17 files changed, 377 insertions(+), 21 deletions(-) create mode 100644 public/app/features/correlations/transformations.ts diff --git a/.betterer.results b/.betterer.results index 9a7a5d883fb..54a896677a0 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3574,9 +3574,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "4"], [0, 0, 0, "Do not use any type assertions.", "5"] ], - "public/app/features/explore/utils/links.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/features/expressions/ExpressionDatasource.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], diff --git a/package.json b/package.json index ee2654967c3..ad7dde315ab 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "@types/js-yaml": "^4.0.5", "@types/jsurl": "^1.2.28", "@types/lodash": "4.14.187", - "@types/logfmt": "^1.2.1", + "@types/logfmt": "^1.2.3", "@types/mousetrap": "1.6.10", "@types/node": "18.14.0", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.0.6", diff --git a/packages/grafana-data/src/types/ScopedVars.ts b/packages/grafana-data/src/types/ScopedVars.ts index b066d3a0a88..2d953c4a840 100644 --- a/packages/grafana-data/src/types/ScopedVars.ts +++ b/packages/grafana-data/src/types/ScopedVars.ts @@ -1,5 +1,5 @@ export interface ScopedVar { - text: any; + text?: any; value: T; [key: string]: any; } diff --git a/packages/grafana-data/src/types/dataLink.ts b/packages/grafana-data/src/types/dataLink.ts index 13e33675dc3..3b9db274d94 100644 --- a/packages/grafana-data/src/types/dataLink.ts +++ b/packages/grafana-data/src/types/dataLink.ts @@ -40,12 +40,27 @@ export interface DataLink { internal?: InternalDataLink; } +/** @internal */ +export enum SupportedTransformationTypes { + Regex = 'regex', + Logfmt = 'logfmt', +} + +/** @internal */ +export interface DataLinkTransformationConfig { + type: SupportedTransformationTypes; + field?: string; + expression?: string; + mapValue?: string; +} + /** @internal */ export interface InternalDataLink { query: T; datasourceUid: string; datasourceName: string; // used as a title if `DataLink.title` is empty panelsState?: ExplorePanelsState; + transformations?: DataLinkTransformationConfig[]; } export type LinkTarget = '_blank' | '_self' | undefined; diff --git a/pkg/services/correlations/database.go b/pkg/services/correlations/database.go index 29c3dedd276..62897adb424 100644 --- a/pkg/services/correlations/database.go +++ b/pkg/services/correlations/database.go @@ -129,6 +129,9 @@ func (s CorrelationsService) updateCorrelation(ctx context.Context, cmd UpdateCo if cmd.Config.Target != nil { correlation.Config.Target = *cmd.Config.Target } + if cmd.Config.Transformations != nil { + correlation.Config.Transformations = cmd.Config.Transformations + } } updateCount, err := session.Where("uid = ? AND source_uid = ?", correlation.UID, correlation.SourceUID).Limit(1).Update(correlation) diff --git a/pkg/services/correlations/models.go b/pkg/services/correlations/models.go index 58e3ebcb247..9d5b283b2b5 100644 --- a/pkg/services/correlations/models.go +++ b/pkg/services/correlations/models.go @@ -14,10 +14,21 @@ var ( ErrCorrelationNotFound = errors.New("correlation not found") ErrUpdateCorrelationEmptyParams = errors.New("not enough parameters to edit correlation") ErrInvalidConfigType = errors.New("invalid correlation config type") + ErrInvalidTransformationType = errors.New("invalid transformation type") + ErrTransformationNotNested = errors.New("transformations must be nested under config") + ErrTransformationRegexReqExp = errors.New("regex transformations require expression") ) type CorrelationConfigType string +type Transformation struct { + //Enum: regex,logfmt + Type string `json:"type"` + Expression string `json:"expression,omitempty"` + Field string `json:"field,omitempty"` + MapValue string `json:"mapValue,omitempty"` +} + const ( ConfigTypeQuery CorrelationConfigType = "query" ) @@ -29,6 +40,19 @@ func (t CorrelationConfigType) Validate() error { return nil } +func (t Transformations) Validate() error { + for _, v := range t { + if v.Type != "regex" && v.Type != "logfmt" { + return fmt.Errorf("%s: \"%s\"", ErrInvalidTransformationType, t) + } else if v.Type == "regex" && len(v.Expression) == 0 { + return fmt.Errorf("%s: \"%s\"", ErrTransformationRegexReqExp, t) + } + } + return nil +} + +type Transformations []Transformation + // swagger:model type CorrelationConfig struct { // Field used to attach the correlation link @@ -42,21 +66,28 @@ type CorrelationConfig struct { // required:true // example: { "expr": "job=app" } Target map[string]interface{} `json:"target" binding:"Required"` + // Source data transformations + // required:false + // example: [{"type": "logfmt"}] + Transformations Transformations `json:"transformations,omitempty"` } func (c CorrelationConfig) MarshalJSON() ([]byte, error) { target := c.Target + transformations := c.Transformations if target == nil { target = map[string]interface{}{} } return json.Marshal(struct { - Type CorrelationConfigType `json:"type"` - Field string `json:"field"` - Target map[string]interface{} `json:"target"` + Type CorrelationConfigType `json:"type"` + Field string `json:"field"` + Target map[string]interface{} `json:"target"` + Transformations Transformations `json:"transformations,omitempty"` }{ - Type: ConfigTypeQuery, - Field: c.Field, - Target: target, + Type: ConfigTypeQuery, + Field: c.Field, + Target: target, + Transformations: transformations, }) } @@ -117,6 +148,10 @@ func (c CreateCorrelationCommand) Validate() error { if c.TargetUID == nil && c.Config.Type == ConfigTypeQuery { return fmt.Errorf("correlations of type \"%s\" must have a targetUID", ConfigTypeQuery) } + + if err := c.Config.Transformations.Validate(); err != nil { + return err + } return nil } @@ -151,6 +186,9 @@ type CorrelationConfigUpdateDTO struct { // Target data query // example: { "expr": "job=app" } Target *map[string]interface{} `json:"target"` + // Source data transformations + // example: [{"type": "logfmt"},{"type":"regex","expression":"(Superman|Batman)", "variable":"name"}] + Transformations []Transformation `json:"transformations"` } func (c CorrelationConfigUpdateDTO) Validate() error { diff --git a/pkg/services/provisioning/datasources/datasources.go b/pkg/services/provisioning/datasources/datasources.go index 0c3ce3deef7..cf5128b0219 100644 --- a/pkg/services/provisioning/datasources/datasources.go +++ b/pkg/services/provisioning/datasources/datasources.go @@ -152,6 +152,10 @@ func makeCreateCorrelationCommand(correlation map[string]interface{}, SourceUID createCommand.TargetUID = &targetUID } + if correlation["transformations"] != nil { + return correlations.CreateCorrelationCommand{}, correlations.ErrTransformationNotNested + } + if correlation["config"] != nil { jsonbody, err := json.Marshal(correlation["config"]) if err != nil { diff --git a/pkg/tests/api/correlations/correlations_create_test.go b/pkg/tests/api/correlations/correlations_create_test.go index 978f285bf3c..3bf809bc3de 100644 --- a/pkg/tests/api/correlations/correlations_create_test.go +++ b/pkg/tests/api/correlations/correlations_create_test.go @@ -237,6 +237,8 @@ func TestIntegrationCreateCorrelation(t *testing.T) { label := "a label" fieldName := "fieldName" configType := correlations.ConfigTypeQuery + transformation := correlations.Transformation{Type: "logfmt"} + transformation2 := correlations.Transformation{Type: "regex", Expression: "testExpression", MapValue: "testVar"} res := ctx.Post(PostParams{ url: fmt.Sprintf("/api/datasources/uid/%s/correlations", writableDs), body: fmt.Sprintf(`{ @@ -246,7 +248,11 @@ func TestIntegrationCreateCorrelation(t *testing.T) { "config": { "type": "%s", "field": "%s", - "target": { "expr": "foo" } + "target": { "expr": "foo" }, + "transformations": [ + {"type": "logfmt"}, + {"type": "regex", "expression": "testExpression", "mapValue": "testVar"} + ] } }`, writableDs, description, label, configType, fieldName), user: adminUser, @@ -268,6 +274,8 @@ func TestIntegrationCreateCorrelation(t *testing.T) { require.Equal(t, configType, response.Result.Config.Type) require.Equal(t, fieldName, response.Result.Config.Field) require.Equal(t, map[string]interface{}{"expr": "foo"}, response.Result.Config.Target) + require.Equal(t, transformation, response.Result.Config.Transformations[0]) + require.Equal(t, transformation2, response.Result.Config.Transformations[1]) require.NoError(t, res.Body.Close()) }) diff --git a/pkg/tests/api/correlations/correlations_read_test.go b/pkg/tests/api/correlations/correlations_read_test.go index 1808916c78d..8505244ac45 100644 --- a/pkg/tests/api/correlations/correlations_read_test.go +++ b/pkg/tests/api/correlations/correlations_read_test.go @@ -79,6 +79,9 @@ func TestIntegrationReadCorrelation(t *testing.T) { Type: correlations.ConfigTypeQuery, Field: "foo", Target: map[string]interface{}{}, + Transformations: []correlations.Transformation{ + {Type: "logfmt"}, + }, }, }) diff --git a/pkg/tests/api/correlations/correlations_update_test.go b/pkg/tests/api/correlations/correlations_update_test.go index 50a6a810016..41acbfc4f87 100644 --- a/pkg/tests/api/correlations/correlations_update_test.go +++ b/pkg/tests/api/correlations/correlations_update_test.go @@ -287,7 +287,8 @@ func TestIntegrationUpdateCorrelation(t *testing.T) { "config": { "field": "field", "type": "query", - "target": { "expr": "bar" } + "target": { "expr": "bar" }, + "transformations": [ {"type": "logfmt"} ] } }`, }) @@ -305,6 +306,7 @@ func TestIntegrationUpdateCorrelation(t *testing.T) { require.Equal(t, "1", response.Result.Description) require.Equal(t, "field", response.Result.Config.Field) require.Equal(t, map[string]interface{}{"expr": "bar"}, response.Result.Config.Target) + require.Equal(t, correlations.Transformation{Type: "logfmt"}, response.Result.Config.Transformations[0]) require.NoError(t, res.Body.Close()) // partially updating only label diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 2237ee3eb56..22eeca3a8c6 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -192,7 +192,7 @@ export const safeParseJson = (text?: string): any | undefined => { }; export const safeStringifyValue = (value: any, space?: number) => { - if (!value) { + if (value === undefined || value === null) { return ''; } diff --git a/public/app/features/correlations/transformations.ts b/public/app/features/correlations/transformations.ts new file mode 100644 index 00000000000..86d9c98cb5b --- /dev/null +++ b/public/app/features/correlations/transformations.ts @@ -0,0 +1,34 @@ +import logfmt from 'logfmt'; + +import { ScopedVars, DataLinkTransformationConfig, SupportedTransformationTypes } from '@grafana/data'; +import { safeStringifyValue } from 'app/core/utils/explore'; + +export const getTransformationVars = ( + transformation: DataLinkTransformationConfig, + fieldValue: string, + fieldName: string +): ScopedVars => { + let transformationScopedVars: ScopedVars = {}; + let transformVal: { [key: string]: string | boolean | null | undefined } = {}; + if (transformation.type === SupportedTransformationTypes.Regex && transformation.expression) { + const regexp = new RegExp(transformation.expression, 'gi'); + const matches = fieldValue.matchAll(regexp); + for (const match of matches) { + if (match.groups) { + transformVal = match.groups; + } else { + transformVal[transformation.mapValue || fieldName] = match[1] || match[0]; + } + } + } else if (transformation.type === SupportedTransformationTypes.Logfmt) { + transformVal = logfmt.parse(fieldValue); + } + + Object.keys(transformVal).forEach((key) => { + const transformValString = + typeof transformVal[key] === 'string' ? transformVal[key] : safeStringifyValue(transformVal[key]); + transformationScopedVars[key] = { value: transformValString }; + }); + + return transformationScopedVars; +}; diff --git a/public/app/features/correlations/types.ts b/public/app/features/correlations/types.ts index 03789340cbd..ae3b6950b4c 100644 --- a/public/app/features/correlations/types.ts +++ b/public/app/features/correlations/types.ts @@ -1,3 +1,5 @@ +import { DataLinkTransformationConfig } from '@grafana/data'; + export interface AddCorrelationResponse { correlation: Correlation; } @@ -5,10 +7,12 @@ export interface AddCorrelationResponse { export type GetCorrelationsResponse = Correlation[]; type CorrelationConfigType = 'query'; + export interface CorrelationConfig { field: string; target: object; type: CorrelationConfigType; + transformations?: DataLinkTransformationConfig[]; } export interface Correlation { diff --git a/public/app/features/correlations/utils.ts b/public/app/features/correlations/utils.ts index 537698efee3..98bf36654f8 100644 --- a/public/app/features/correlations/utils.ts +++ b/public/app/features/correlations/utils.ts @@ -39,6 +39,7 @@ const decorateDataFrameWithInternalDataLinks = (dataFrame: DataFrame, correlatio query: correlation.config?.target, datasourceUid: correlation.target.uid, datasourceName: correlation.target.name, + transformations: correlation.config?.transformations, }, url: '', title: correlation.label || correlation.target.name, diff --git a/public/app/features/explore/utils/links.test.ts b/public/app/features/explore/utils/links.test.ts index 4bb06ec6817..e537de76d7f 100644 --- a/public/app/features/explore/utils/links.test.ts +++ b/public/app/features/explore/utils/links.test.ts @@ -6,6 +6,7 @@ import { Field, FieldType, InterpolateFunction, + SupportedTransformationTypes, TimeRange, toDataFrame, } from '@grafana/data'; @@ -240,6 +241,233 @@ describe('getFieldLinksForExplore', () => { ); }); + it('returns internal links with logfmt and regex transformation', () => { + const transformationLink: DataLink = { + title: '', + url: '', + internal: { + query: { query: 'http_requests{app=${application} env=${environment}}' }, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', + transformations: [ + { type: SupportedTransformationTypes.Logfmt }, + { type: SupportedTransformationTypes.Regex, expression: 'host=(dev|prod)', mapValue: 'environment' }, + ], + }, + }; + + const { field, range, dataFrame } = setup(transformationLink, true, { + name: 'msg', + type: FieldType.string, + values: new ArrayVector(['application=foo host=dev-001', 'application=bar host=prod-003']), + config: { + links: [transformationLink], + }, + }); + + const links = [ + getFieldLinksForExplore({ field, rowIndex: 0, range, dataFrame }), + getFieldLinksForExplore({ field, rowIndex: 1, range, dataFrame }), + ]; + expect(links[0]).toHaveLength(1); + expect(links[0][0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=foo env=dev}"}]}' + )}` + ); + expect(links[1]).toHaveLength(1); + expect(links[1][0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=bar env=prod}"}]}' + )}` + ); + }); + + it('returns internal links with 2 unnamed regex transformations and use the last transformation', () => { + const transformationLink: DataLink = { + title: '', + url: '', + internal: { + query: { query: 'http_requests{env=${msg}}' }, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', + transformations: [ + { type: SupportedTransformationTypes.Regex, expression: 'fieldA=(asparagus|broccoli)' }, + { type: SupportedTransformationTypes.Regex, expression: 'fieldB=(apple|banana)' }, + ], + }, + }; + + const { field, range, dataFrame } = setup(transformationLink, true, { + name: 'msg', + type: FieldType.string, + values: new ArrayVector(['fieldA=asparagus fieldB=banana', 'fieldA=broccoli fieldB=apple']), + config: { + links: [transformationLink], + }, + }); + + const links = [ + getFieldLinksForExplore({ field, rowIndex: 0, range, dataFrame }), + getFieldLinksForExplore({ field, rowIndex: 1, range, dataFrame }), + ]; + expect(links[0]).toHaveLength(1); + expect(links[0][0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{env=banana}"}]}' + )}` + ); + expect(links[1]).toHaveLength(1); + expect(links[1][0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{env=apple}"}]}' + )}` + ); + }); + + it('returns internal links with logfmt with stringified booleans', () => { + const transformationLink: DataLink = { + title: '', + url: '', + internal: { + query: { query: 'http_requests{app=${application} isOnline=${online}}' }, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', + transformations: [{ type: SupportedTransformationTypes.Logfmt }], + }, + }; + + const { field, range, dataFrame } = setup(transformationLink, true, { + name: 'msg', + type: FieldType.string, + values: new ArrayVector(['application=foo online=true', 'application=bar online=false']), + config: { + links: [transformationLink], + }, + }); + + const links = [ + getFieldLinksForExplore({ field, rowIndex: 0, range, dataFrame }), + getFieldLinksForExplore({ field, rowIndex: 1, range, dataFrame }), + ]; + expect(links[0]).toHaveLength(1); + expect(links[0][0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=foo isOnline=true}"}]}' + )}` + ); + expect(links[1]).toHaveLength(1); + expect(links[1][0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=bar isOnline=false}"}]}' + )}` + ); + }); + + it('returns internal links with logfmt with correct data on transformation-defined field', () => { + const transformationLink: DataLink = { + title: '', + url: '', + internal: { + query: { query: 'http_requests{app=${application}}' }, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', + transformations: [{ type: SupportedTransformationTypes.Logfmt, field: 'fieldNamedInTransformation' }], + }, + }; + + // fieldWithLink has the transformation, but the transformation has defined fieldNamedInTransformation as its field to transform + const { field, range, dataFrame } = setup( + transformationLink, + true, + { + name: 'fieldWithLink', + type: FieldType.string, + values: new ArrayVector(['application=link', 'application=link2']), + config: { + links: [transformationLink], + }, + }, + [ + { + name: 'fieldNamedInTransformation', + type: FieldType.string, + values: new ArrayVector(['application=transform', 'application=transform2']), + config: {}, + }, + ] + ); + + const links = [ + getFieldLinksForExplore({ field, rowIndex: 0, range, dataFrame }), + getFieldLinksForExplore({ field, rowIndex: 1, range, dataFrame }), + ]; + expect(links[0]).toHaveLength(1); + expect(links[0][0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=transform}"}]}' + )}` + ); + expect(links[1]).toHaveLength(1); + expect(links[1][0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=transform2}"}]}' + )}` + ); + }); + + it('returns internal links with regex named capture groups', () => { + const transformationLink: DataLink = { + title: '', + url: '', + internal: { + query: { query: 'http_requests{app=${application} env=${environment}}' }, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', + transformations: [ + { + type: SupportedTransformationTypes.Regex, + expression: '(?=.*(?(grafana|loki)))(?=.*(?(dev|prod)))', + }, + ], + }, + }; + + const { field, range, dataFrame } = setup(transformationLink, true, { + name: 'msg', + type: FieldType.string, + values: new ArrayVector(['foo loki prod', 'dev bar grafana', 'prod grafana foo']), + config: { + links: [transformationLink], + }, + }); + + const links = [ + getFieldLinksForExplore({ field, rowIndex: 0, range, dataFrame }), + getFieldLinksForExplore({ field, rowIndex: 1, range, dataFrame }), + getFieldLinksForExplore({ field, rowIndex: 2, range, dataFrame }), + ]; + expect(links[0]).toHaveLength(1); + expect(links[0][0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=loki env=prod}"}]}' + )}` + ); + expect(links[1]).toHaveLength(1); + expect(links[1][0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=grafana env=dev}"}]}' + )}` + ); + + expect(links[2]).toHaveLength(1); + expect(links[2][0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=grafana env=prod}"}]}' + )}` + ); + }); + it('returns no internal links when target contains empty template variables', () => { const { field, range, dataFrame } = setup({ title: '', diff --git a/public/app/features/explore/utils/links.ts b/public/app/features/explore/utils/links.ts index 6c50fb4e312..7e88f20222e 100644 --- a/public/app/features/explore/utils/links.ts +++ b/public/app/features/explore/utils/links.ts @@ -15,6 +15,7 @@ import { } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; +import { getTransformationVars } from 'app/features/correlations/transformations'; import { getLinkSrv } from '../../panel/panellinks/link_srv'; @@ -71,7 +72,7 @@ export const getFieldLinksForExplore = (options: { dataFrame?: DataFrame; }): Array> => { const { field, vars, splitOpenFn, range, rowIndex, dataFrame } = options; - const scopedVars: any = { ...(vars || {}) }; + const scopedVars: ScopedVars = { ...(vars || {}) }; scopedVars['__value'] = { value: { raw: field.values.get(rowIndex), @@ -127,10 +128,28 @@ export const getFieldLinksForExplore = (options: { } return linkModel; } 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.get(rowIndex); + } else { + fieldValue = field.values.get(rowIndex); + } + + internalLinkSpecificVars = { + ...internalLinkSpecificVars, + ...getTransformationVars(transformation, fieldValue, field.name), + }; + }); + } + return mapInternalLinkToExplore({ link, internalLink: link.internal, - scopedVars: scopedVars, + scopedVars: { ...scopedVars, ...internalLinkSpecificVars }, range, field, onClickFn: splitOpenFn, diff --git a/yarn.lock b/yarn.lock index af9d9d6a4e2..efd02d6d19a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11497,12 +11497,12 @@ __metadata: languageName: node linkType: hard -"@types/logfmt@npm:^1.2.1": - version: 1.2.2 - resolution: "@types/logfmt@npm:1.2.2" +"@types/logfmt@npm:^1.2.3": + version: 1.2.3 + resolution: "@types/logfmt@npm:1.2.3" dependencies: "@types/node": "*" - checksum: 3f75ce7cc79886b6c3a2b7c467c49d72752a494cf316b87315c7609f9d3f29ffc6d400b6ae9e81c3997ef1e8b0a442974b60e86b3e45a8174b9b0415aba40701 + checksum: d5872ab0432c687dc95a4c3a1c21c8eca24553415ef6a34f6cbbe0eefc4b7b8fb8b2af80df4a53fcf7cc7b212569df568bed1b17f7c2a976c4416f4a67b285de languageName: node linkType: hard @@ -22002,7 +22002,7 @@ __metadata: "@types/js-yaml": ^4.0.5 "@types/jsurl": ^1.2.28 "@types/lodash": 4.14.187 - "@types/logfmt": ^1.2.1 + "@types/logfmt": ^1.2.3 "@types/mousetrap": 1.6.10 "@types/node": 18.14.0 "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.0.6"